diff --git a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlaylistWSController.java b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlaylistWSController.java index 5f6e8652d..e62b1972e 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlaylistWSController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlaylistWSController.java @@ -58,23 +58,26 @@ public List getWritablePlaylists(Principal p) { return playlistService.getWritablePlaylistsForUser(p.getName()); } + /** + * Creates a playlist and broadcasts it to all users that have access to it. + * + * @param playlist the playlist to create + * @return the id of the created playlist + */ + private Playlist createPlaylist(String name, boolean shared, String username) { + Playlist result = playlistService.createPlaylist(name, shared, username); + playlistService.broadcast(result); + return result; + } + @MessageMapping("/create/empty") @SendToUser(broadcast = false) public int createEmptyPlaylist(Principal p) { Locale locale = localeResolver.resolveLocale(p.getName()); DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale); - Instant now = Instant.now(); - Playlist playlist = new Playlist(); - playlist.setUsername(p.getName()); - playlist.setCreated(now); - playlist.setChanged(now); - playlist.setShared(false); - playlist.setName(dateFormat.format(now.atZone(ZoneId.systemDefault()))); - - playlistService.createPlaylist(playlist); - - return playlist.getId(); + Playlist result = createPlaylist(dateFormat.format(now.atZone(ZoneId.systemDefault())), false, p.getName()); + return result.getId(); } @MessageMapping("/create/starred") @@ -83,22 +86,18 @@ public int createPlaylistForStarredSongs(Principal p) { Locale locale = localeResolver.resolveLocale(p.getName()); DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale); - Instant now = Instant.now(); - Playlist playlist = new Playlist(); - playlist.setUsername(p.getName()); - playlist.setCreated(now); - playlist.setChanged(now); - playlist.setShared(false); - ResourceBundle bundle = ResourceBundle.getBundle("org.airsonic.player.i18n.ResourceBundle", locale); - playlist.setName(bundle.getString("top.starred") + " " + dateFormat.format(now.atZone(ZoneId.systemDefault()))); - - playlistService.createPlaylist(playlist); - List musicFolders = mediaFolderService.getMusicFoldersForUser(p.getName()); - List songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, p.getName(), musicFolders); - playlistService.setFilesInPlaylist(playlist.getId(), songs); - - return playlist.getId(); + Instant now = Instant.now(); + String name = bundle.getString("top.starred") + " " + dateFormat.format(now.atZone(ZoneId.systemDefault())); + String username = p.getName(); + + Playlist result = createPlaylist(name, false, username); + List musicFolders = mediaFolderService.getMusicFoldersForUser(username); + List songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders); + Integer playlistId = result.getId(); + playlistService.setFilesInPlaylist(playlistId, songs); + playlistService.broadcastFileChange(playlistId, false, true); + return result.getId(); } @MessageMapping("/create/playqueue") @@ -111,29 +110,31 @@ public int createPlaylistForPlayQueue(Principal p, Integer playerId) throws Exce Instant now = Instant.now(); Playlist playlist = new Playlist(); playlist.setUsername(p.getName()); - playlist.setCreated(now); - playlist.setChanged(now); playlist.setShared(false); playlist.setName(dateFormat.format(now.atZone(ZoneId.systemDefault()))); - playlistService.createPlaylist(playlist); - playlistService.setFilesInPlaylist(playlist.getId(), player.getPlayQueue().getFiles()); + Playlist result = createPlaylist(dateFormat.format(now.atZone(ZoneId.systemDefault())), false, p.getName()); + Integer playlistId = result.getId(); + playlistService.setFilesInPlaylist(playlistId, player.getPlayQueue().getFiles()); + playlistService.broadcastFileChange(playlistId, false, true); - return playlist.getId(); + return playlistId; } @MessageMapping("/delete") public void deletePlaylist(int id) { playlistService.deletePlaylist(id); + playlistService.broadcastDeleted(id); } @MessageMapping("/update") public void updatePlaylist(PlaylistUpdateRequest req) { - Playlist playlist = new Playlist(playlistService.getPlaylist(req.getId())); - playlist.setName(req.getName()); - playlist.setComment(req.getComment()); - playlist.setShared(req.getShared()); - playlistService.updatePlaylist(playlist); + Playlist playlist = playlistService.getPlaylist(req.getId()); + if (playlist == null) { + return; + } + playlistService.updatePlaylist(req.getId(), req.getName(), req.getComment(), req.getShared()); + playlistService.broadcastFileChange(req.getId(), playlist.getShared(), false); } @MessageMapping("/files/append") @@ -146,6 +147,7 @@ public int appendToPlaylist(PlaylistFilesModificationRequest req) { .collect(Collectors.toList()); playlistService.setFilesInPlaylist(req.getId(), files); + playlistService.broadcastFileChange(req.getId(), false, true); return req.getId(); } @@ -154,15 +156,10 @@ public int appendToPlaylist(PlaylistFilesModificationRequest req) { @SendToUser(broadcast = false) public int remove(PlaylistFilesModificationRequest req) { // in this context, modifierIds are indices - List files = playlistService.getFilesInPlaylist(req.getId(), true); List indices = req.getModifierIds(); Collections.sort(indices); - for (int i = 0; i < indices.size(); i++) { - // factor in previous indices we've deleted so far - files.remove(indices.get(i) - i); - } - playlistService.setFilesInPlaylist(req.getId(), files); - + playlistService.removeFilesInPlaylistByIndices(req.getId(), indices); + playlistService.broadcastFileChange(req.getId(), false, true); return req.getId(); } @@ -174,6 +171,7 @@ public int up(PlaylistFilesModificationRequest req) { if (req.getModifierIds().size() == 1 && req.getModifierIds().get(0) > 0) { Collections.swap(files, req.getModifierIds().get(0), req.getModifierIds().get(0) - 1); playlistService.setFilesInPlaylist(req.getId(), files); + playlistService.broadcastFileChange(req.getId(), false, true); } return req.getId(); @@ -187,6 +185,7 @@ public int down(PlaylistFilesModificationRequest req) { if (req.getModifierIds().size() == 1 && req.getModifierIds().get(0) < files.size() - 1) { Collections.swap(files, req.getModifierIds().get(0), req.getModifierIds().get(0) + 1); playlistService.setFilesInPlaylist(req.getId(), files); + playlistService.broadcastFileChange(req.getId(), false, true); } return req.getId(); @@ -202,6 +201,7 @@ public int rearrange(PlaylistFilesModificationRequest req) { newFiles[i] = files.get(req.getModifierIds().get(i)); } playlistService.setFilesInPlaylist(req.getId(), Arrays.asList(newFiles)); + playlistService.broadcastFileChange(req.getId(), false, true); return req.getId(); } @@ -217,26 +217,6 @@ public List getPlaylistEntries(Principal p, @DestinationVariable return mediaFileService.toMediaFileEntryList(playlistService.getFilesInPlaylist(id, true), p.getName(), true, true, null, null, null); } - public void setPlaylistService(PlaylistService playlistService) { - this.playlistService = playlistService; - } - - public void setMediaFileService(MediaFileService mediaFileService) { - this.mediaFileService = mediaFileService; - } - - public void setMediaFileDao(MediaFileDao mediaFileDao) { - this.mediaFileDao = mediaFileDao; - } - - public void setPlayerService(PlayerService playerService) { - this.playerService = playerService; - } - - public void setLocaleResolver(LocaleResolver localeResolver) { - this.localeResolver = localeResolver; - } - public static class PlaylistFilesModificationRequest { private int id; private List modifierIds; diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/ExportPlayListController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/ExportPlayListController.java index d6c30b707..24206618c 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/ExportPlayListController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/ExportPlayListController.java @@ -1,6 +1,7 @@ package org.airsonic.player.controller; import org.airsonic.player.domain.Playlist; +import org.airsonic.player.service.PlaylistFileService; import org.airsonic.player.service.PlaylistService; import org.airsonic.player.service.SecurityService; import org.airsonic.player.util.StringUtil; @@ -26,6 +27,9 @@ public class ExportPlayListController { @Autowired private SecurityService securityService; + @Autowired + private PlaylistFileService playlistFileService; + @GetMapping public ModelAndView exportPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { @@ -39,7 +43,7 @@ public ModelAndView exportPlaylist(HttpServletRequest request, HttpServletRespon response.setContentType("application/x-download"); response.setHeader("Content-Disposition", "attachment; filename=\"" + StringUtil.fileSystemSafe(playlist.getName()) + ".m3u8\""); - playlistService.exportPlaylist(id, response.getOutputStream()); + playlistFileService.exportPlaylist(id, response.getOutputStream()); return null; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/GeneralSettingsController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/GeneralSettingsController.java index c6f1f6cb0..231f62845 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/GeneralSettingsController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/GeneralSettingsController.java @@ -21,7 +21,7 @@ import org.airsonic.player.command.GeneralSettingsCommand; import org.airsonic.player.domain.Theme; -import org.airsonic.player.service.PlaylistService; +import org.airsonic.player.service.PlaylistFileService; import org.airsonic.player.service.SettingsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -47,7 +47,7 @@ public class GeneralSettingsController { private SettingsService settingsService; @Autowired - private PlaylistService playlistService; + private PlaylistFileService playlistFileService; @GetMapping protected String displayForm() { @@ -123,8 +123,8 @@ protected String doSubmitAction(@ModelAttribute("command") GeneralSettingsComman settingsService.setGenreSeparators(command.getGenreSeparators()); settingsService.setShortcuts(command.getShortcuts()); settingsService.setPlaylistFolder(command.getPlaylistFolder()); - playlistService.addPlaylistFolderWatcher(); - playlistService.importPlaylists(); + playlistFileService.addPlaylistFolderWatcher(); + playlistFileService.importPlaylists(); settingsService.setMusicFileTypes(command.getMusicFileTypes()); settingsService.setVideoFileTypes(command.getVideoFileTypes()); settingsService.setCoverArtFileTypes(command.getCoverArtFileTypes()); diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/ImportPlaylistController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/ImportPlaylistController.java index ef4be2d94..8232d05df 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/ImportPlaylistController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/ImportPlaylistController.java @@ -21,7 +21,8 @@ package org.airsonic.player.controller; import org.airsonic.player.domain.Playlist; -import org.airsonic.player.service.PlaylistService; +import org.airsonic.player.domain.User; +import org.airsonic.player.service.PlaylistFileService; import org.airsonic.player.service.SecurityService; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; @@ -51,7 +52,7 @@ public class ImportPlaylistController { @Autowired private SecurityService securityService; @Autowired - private PlaylistService playlistService; + private PlaylistFileService playlistFileService; @PostMapping protected String handlePost(@RequestParam("file") MultipartFile file, @@ -67,8 +68,8 @@ protected String handlePost(@RequestParam("file") MultipartFile file, } String playlistName = FilenameUtils.getBaseName(file.getOriginalFilename()); String fileName = FilenameUtils.getName(file.getOriginalFilename()); - String username = securityService.getCurrentUsername(request); - Playlist playlist = playlistService.importPlaylist(username, playlistName, fileName, null, file.getInputStream(), null); + User user = securityService.getCurrentUser(request); + Playlist playlist = playlistFileService.importPlaylist(user, playlistName, fileName, null, file.getInputStream(), null); map.put("playlist", playlist); } else { throw new Exception("No file specified."); diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java index 591058ce9..817f53668 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java @@ -26,6 +26,7 @@ import org.airsonic.player.domain.MediaLibraryStatistics; import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.domain.MusicFolder.Type; +import org.airsonic.player.domain.Playlist; import org.airsonic.player.service.AlbumService; import org.airsonic.player.service.ArtistService; import org.airsonic.player.service.CoverArtService; @@ -145,7 +146,10 @@ private void expunge() { LOG.debug("Deleting non-present media folders..."); mediaFolderService.expunge(); LOG.debug("Refreshing playlist stats..."); - playlistService.refreshPlaylistsStats(); + List playlists = playlistService.refreshPlaylistsStats(); + playlists.forEach(p -> { + playlistService.broadcastFileChange(p.getId(), false, false); + }); LOG.debug("Database cleanup complete."); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java index 4e9e4e050..de810dacb 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java @@ -974,14 +974,8 @@ public void createPlaylist(HttpServletRequest request, HttpServletResponse respo return; } } else { - playlist = new org.airsonic.player.domain.Playlist(); - Instant now = Instant.now(); - playlist.setName(name); - playlist.setCreated(now); - playlist.setChanged(now); - playlist.setShared(false); - playlist.setUsername(username); - playlistService.createPlaylist(playlist); + playlist = playlistService.createPlaylist(name, false, username); + playlistService.broadcast(playlist); } List songs = new ArrayList(); @@ -992,6 +986,7 @@ public void createPlaylist(HttpServletRequest request, HttpServletResponse respo } } playlistService.setFilesInPlaylist(playlist.getId(), songs); + playlistService.broadcastFileChange(playlist.getId(), false, true); writeEmptyResponse(request, response); } @@ -1013,21 +1008,10 @@ public void updatePlaylist(HttpServletRequest request, HttpServletResponse respo } // create new object to not mutate the cache - playlist = new org.airsonic.player.domain.Playlist(playlist); - String name = request.getParameter("name"); - if (name != null) { - playlist.setName(name); - } String comment = request.getParameter("comment"); - if (comment != null) { - playlist.setComment(comment); - } Boolean shared = getBooleanParameter(request, "public"); - if (shared != null) { - playlist.setShared(shared); - } - playlistService.updatePlaylist(playlist); + playlistService.updatePlaylist(id, name, comment, shared); // TODO: Add later // for (String usernameToAdd : ServletRequestUtils.getStringParameters(request, "usernameToAdd")) { @@ -1063,6 +1047,7 @@ public void updatePlaylist(HttpServletRequest request, HttpServletResponse respo if (songsChanged) { playlistService.setFilesInPlaylist(id, songs); } + playlistService.broadcastFileChange(id, playlist.getShared(), songsChanged); writeEmptyResponse(request, response); } @@ -1073,16 +1058,16 @@ public void deletePlaylist(HttpServletRequest request, HttpServletResponse respo String username = securityService.getCurrentUsername(request); int id = getRequiredIntParameter(request, "id"); - org.airsonic.player.domain.Playlist playlist = playlistService.getPlaylist(id); - if (playlist == null) { + if (!playlistService.isExist(id)) { error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); return; } - if (!playlistService.isWriteAllowed(playlist, username)) { + if (!playlistService.isWriteAllowed(id, username)) { error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); return; } playlistService.deletePlaylist(id); + playlistService.broadcastDeleted(id); writeEmptyResponse(request, response); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/PlaylistDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/PlaylistDao.java deleted file mode 100644 index 2045d1ab1..000000000 --- a/airsonic-main/src/main/java/org/airsonic/player/dao/PlaylistDao.java +++ /dev/null @@ -1,149 +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 2016 (C) Airsonic Authors - Based upon Subsonic, Copyright 2009 (C) Sindre Mehus - */ -package org.airsonic.player.dao; - -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.Playlist; -import org.airsonic.player.util.LambdaUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.PostConstruct; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Provides database services for playlists. - * - * @author Sindre Mehus - */ -@Repository -public class PlaylistDao extends AbstractDao { - private static final String INSERT_COLUMNS = "username, is_public, name, comment, file_count, duration, " + - "created, changed, imported_from"; - private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS; - private final PlaylistMapper rowMapper = new PlaylistMapper(); - private final static Comparator sorter = Comparator.comparing(p -> p.getName()); - - @PostConstruct - public void register() throws Exception { - registerInserts("playlist", "id", Arrays.asList(INSERT_COLUMNS.split(", ")), Playlist.class); - } - - public List getReadablePlaylistsForUser(String username) { - - List result1 = getWritablePlaylistsForUser(username); - List result2 = query("select " + QUERY_COLUMNS + " from playlist where is_public", rowMapper); - List result3 = query("select " + prefix(QUERY_COLUMNS, "playlist") + " from playlist, playlist_user where " + - "playlist.id = playlist_user.playlist_id and " + - "playlist.username != ? and " + - "playlist_user.username = ?", rowMapper, username, username); - - // Remove duplicates. - return Stream.of(result1, result2, result3) - .flatMap(r -> r.parallelStream()) - .filter(LambdaUtils.distinctByKey(p -> p.getId())) - .sorted(sorter) - .collect(Collectors.toList()); - } - - public List getWritablePlaylistsForUser(String username) { - return query("select " + QUERY_COLUMNS + " from playlist where username=?", rowMapper, username) - .stream() - .sorted(sorter) - .collect(Collectors.toList()); - } - - public Playlist getPlaylist(int id) { - return queryOne("select " + QUERY_COLUMNS + " from playlist where id=?", rowMapper, id); - } - - public List getAllPlaylists() { - return query("select " + QUERY_COLUMNS + " from playlist", rowMapper) - .stream() - .sorted(sorter) - .collect(Collectors.toList()); - } - - public void createPlaylist(Playlist playlist) { - Integer id = insert("playlist", playlist); - playlist.setId(id); - } - - @Transactional - public void setFilesInPlaylist(int id, List files) { - update("delete from playlist_file where playlist_id=?", id); - batchedUpdate("insert into playlist_file (playlist_id, media_file_id) values (?, ?)", - files.stream().map(x -> new Object[] { id, x.getId() }).collect(Collectors.toList())); - } - - public Pair getPlaylistFileStats(int id) { - return queryOne("select count(*), sum(duration) from media_file m, playlist_file p where p.media_file_id = m.id and p.playlist_id=?", (rs, i) -> Pair.of(rs.getInt(1), rs.getDouble(2)), id); - } - - public List getPlaylistUsers(int playlistId) { - return queryForStrings("select username from playlist_user where playlist_id=?", playlistId); - } - - public void addPlaylistUser(int playlistId, String username) { - if (!getPlaylistUsers(playlistId).contains(username)) { - update("insert into playlist_user(playlist_id,username) values (?,?)", playlistId, username); - } - } - - public void deletePlaylistUser(int playlistId, String username) { - update("delete from playlist_user where playlist_id=? and username=?", playlistId, username); - } - - public void deletePlaylist(int id) { - update("delete from playlist where id=?", id); - } - - public void updatePlaylist(Playlist playlist) { - update("update playlist set username=?, is_public=?, name=?, comment=?, changed=?, imported_from=?, file_count=?, duration=? where id=?", - playlist.getUsername(), playlist.getShared(), playlist.getName(), playlist.getComment(), - Instant.now(), playlist.getImportedFrom(), playlist.getFileCount(), playlist.getDuration(), - playlist.getId()); - } - - private static class PlaylistMapper implements RowMapper { - @Override - public Playlist mapRow(ResultSet rs, int rowNum) throws SQLException { - return new Playlist( - rs.getInt(1), - rs.getString(2), - rs.getBoolean(3), - rs.getString(4), - rs.getString(5), - rs.getInt(6), - rs.getDouble(7), - Optional.ofNullable(rs.getTimestamp(8)).map(x -> x.toInstant()).orElse(null), - Optional.ofNullable(rs.getTimestamp(9)).map(x -> x.toInstant()).orElse(null), - rs.getString(10)); - } - } -} 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 9b617c215..f43bb30cf 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 @@ -19,33 +19,60 @@ */ package org.airsonic.player.domain; -import org.airsonic.player.dao.AbstractDao.Column; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.persistence.*; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; /** * @author Sindre Mehus */ +@Entity +@Table(name = "playlist") public class Playlist { - private int id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @Column(name = "username", nullable = false) private String username; - @Column("is_public") + @Column(name = "is_public") private boolean shared; + @Column(name = "name", nullable = false) private String name; + @Column(name = "comment") private String comment; + @Column(name = "file_count") private int fileCount; + @Column(name = "duration") private double duration; + @Column(name = "created") private Instant created; + @Column(name = "changed") private Instant changed; + @Column(name = "imported_from") private String importedFrom; + @ManyToMany + @JoinTable(name = "playlist_user", + joinColumns = @JoinColumn(name = "playlist_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "username")) + private List sharedUsers; + + @ManyToMany + @JoinTable(name = "playlist_file", + joinColumns = @JoinColumn(name = "playlist_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "media_file_id", referencedColumnName = "id")) + private List mediaFiles; + public Playlist() { } - public Playlist(int id, String username, boolean shared, String name, String comment, int fileCount, + public Playlist(String username, boolean shared, String name, String comment, int fileCount, double duration, Instant created, Instant changed, String importedFrom) { - this.id = id; this.username = username; this.shared = shared; this.name = name; @@ -58,15 +85,15 @@ public Playlist(int id, String username, boolean shared, String name, String com } public Playlist(Playlist p) { - this(p.getId(), p.getUsername(), p.getShared(), p.getName(), p.getComment(), p.getFileCount(), p.getDuration(), + this(p.getUsername(), p.getShared(), p.getName(), p.getComment(), p.getFileCount(), p.getDuration(), p.getCreated(), p.getChanged(), p.getImportedFrom()); } - public int getId() { + public Integer getId() { return id; } - public void setId(int id) { + public void setId(Integer id) { this.id = id; } @@ -141,4 +168,47 @@ public String getImportedFrom() { public void setImportedFrom(String importedFrom) { this.importedFrom = importedFrom; } + + @JsonIgnore + public List getSharedUsers() { + return sharedUsers; + } + + public void setSharedUsers(List sharedUsers) { + if (this.sharedUsers == null) { + this.sharedUsers = new ArrayList<>(); + } else { + this.sharedUsers.clear(); + } + this.sharedUsers.addAll(sharedUsers); + } + + public void addSharedUser(User sharedUser) { + this.sharedUsers.add(sharedUser); + } + + public void removeSharedUser(User sharedUser) { + this.sharedUsers.remove(sharedUser); + } + + public void removeSharedUserByUsername(String username) { + this.sharedUsers.removeIf(su -> su.getUsername().equals(username)); + } + + @JsonIgnore + public List getMediaFiles() { + return mediaFiles; + } + + public void setMediaFiles(List mediaFiles) { + if (this.mediaFiles == null) { + this.mediaFiles = new ArrayList<>(); + } else { + this.mediaFiles.clear(); + } + if (mediaFiles != null) { + this.mediaFiles.addAll(mediaFiles); + } + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/PlaylistRepository.java b/airsonic-main/src/main/java/org/airsonic/player/repository/PlaylistRepository.java new file mode 100644 index 000000000..97ebd2d69 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/repository/PlaylistRepository.java @@ -0,0 +1,25 @@ +package org.airsonic.player.repository; + +import org.airsonic.player.domain.Playlist; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PlaylistRepository extends JpaRepository { + + public List findByUsername(String username); + + public List findByUsernameOrderByNameAsc(String username); + + public List findBySharedTrue(); + + public List findByUsernameNotAndSharedUsersUsername(String username, String sharedUsername); + + public Optional findByIdAndSharedUsersUsername(Integer id, String sharedUsername); + + public boolean existsByIdAndUsername(Integer id, String username); + +} \ No newline at end of file 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 c6dc814b9..ad2687e12 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 @@ -67,7 +67,7 @@ public class MediaScannerService { public MediaScannerService( SettingsService settingsService, IndexManager indexManager, - PlaylistService playlistService, + PlaylistFileService playlistFileService, MediaFileService mediaFileService, MediaFolderService mediaFolderService, CoverArtService coverArtService, @@ -80,7 +80,7 @@ public MediaScannerService( ) { this.settingsService = settingsService; this.indexManager = indexManager; - this.playlistService = playlistService; + this.playlistFileService = playlistFileService; this.mediaFileService = mediaFileService; this.mediaFolderService = mediaFolderService; this.coverArtService = coverArtService; @@ -95,7 +95,7 @@ public MediaScannerService( private final SettingsService settingsService; private final IndexManager indexManager; - private final PlaylistService playlistService; + private final PlaylistFileService playlistFileService; private final MediaFileService mediaFileService; private final MediaFolderService mediaFolderService; private final CoverArtService coverArtService; @@ -219,7 +219,7 @@ public synchronized void scanLibrary() { LOG.info("Media library scan completed."); } }) - .thenRunAsync(() -> playlistService.importPlaylists(), pool) + .thenRunAsync(() -> playlistFileService.importPlaylists(), pool) .whenComplete((r,e) -> { pool.shutdown(); }) diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistFileService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistFileService.java new file mode 100644 index 000000000..b0f8d8862 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistFileService.java @@ -0,0 +1,221 @@ +package org.airsonic.player.service; + +import chameleon.playlist.SpecificPlaylist; +import chameleon.playlist.SpecificPlaylistFactory; +import chameleon.playlist.SpecificPlaylistProvider; +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.domain.Playlist; +import org.airsonic.player.domain.User; +import org.airsonic.player.repository.UserRepository; +import org.airsonic.player.service.playlist.PlaylistExportHandler; +import org.airsonic.player.service.playlist.PlaylistImportHandler; +import org.airsonic.player.util.StringUtil; +import org.airsonic.player.util.UserUtil; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.WatchEvent; +import java.util.*; +import java.util.stream.Stream; + +@Service +public class PlaylistFileService { + + private static final Logger LOG = LoggerFactory.getLogger(PlaylistService.class); + + private final PlaylistService playlistService; + private final SettingsService settingsService; + private final UserRepository userRepository; + private final PathWatcherService pathWatcherService; + private final List exportHandlers; + private final List importHandlers; + + @Autowired + public PlaylistFileService(PlaylistService playlistService, + SettingsService settingsService, + UserRepository userRepository, + PathWatcherService pathWatcherService, + List exportHandlers, + List importHandlers) { + this.playlistService = playlistService; + this.settingsService = settingsService; + this.userRepository = userRepository; + this.pathWatcherService = pathWatcherService; + this.exportHandlers = exportHandlers; + this.importHandlers = importHandlers; + } + + + @PostConstruct + public void init() throws IOException { + addPlaylistFolderWatcher(); + } + + public void addPlaylistFolderWatcher() { + Path playlistFolder = Paths.get(settingsService.getPlaylistFolder()); + if (Files.exists(playlistFolder) && Files.isDirectory(playlistFolder)) { + try { + pathWatcherService.setWatcher("Playlist folder watcher", playlistFolder, + this::handleModifiedPlaylist, null, this::handleModifiedPlaylist, null); + } catch (Exception e) { + LOG.warn("Issues setting watcher for folder: {}", playlistFolder); + } + } + } + + private void handleModifiedPlaylist(Path path, WatchEvent event) { + Path fullPath = path.resolve(event.context()); + importPlaylist(fullPath, playlistService.getAllPlaylists()); + } + + public void importPlaylists() { + try { + LOG.info("Starting playlist import."); + doImportPlaylists(); + LOG.info("Completed playlist import."); + } catch (Throwable x) { + LOG.warn("Failed to import playlists: " + x, x); + } + } + + private void doImportPlaylists() { + String playlistFolderPath = settingsService.getPlaylistFolder(); + if (playlistFolderPath == null) { + return; + } + Path playlistFolder = Paths.get(playlistFolderPath); + if (!Files.exists(playlistFolder)) { + return; + } + + List allPlaylists = playlistService.getAllPlaylists(); + try (Stream children = Files.walk(playlistFolder)) { + children.forEach(f -> importPlaylist(f, allPlaylists)); + } catch (IOException ex) { + LOG.warn("Error while reading directory {} when importing playlists", playlistFolder, ex); + } + } + + private void importPlaylist(Path f, List allPlaylists) { + if (Files.isRegularFile(f) && Files.isReadable(f)) { + try { + Playlist playlist = allPlaylists.stream() + .filter(p -> f.getFileName().toString().equals(p.getImportedFrom())) + .findAny().orElse(null); + importPlaylistIfUpdated(f, playlist); + } catch (Exception x) { + LOG.warn("Failed to auto-import playlist {}", f, x); + } + } + } + + /** + * Import a playlist from a file. + * + * @param user user importing the playlist + * @param playlistName name of the playlist + * @param fileName name of the file + * @param file path to the file + * @param inputStream input stream to the file + * @param existingPlaylist existing playlist to update, or null to create a new one + * @return the imported playlist + * @throws Exception + */ + public Playlist importPlaylist(User user, String playlistName, String fileName, Path file, InputStream inputStream, Playlist existingPlaylist) throws Exception { + + // TODO: handle other encodings + final SpecificPlaylist inputSpecificPlaylist = SpecificPlaylistFactory.getInstance().readFrom(inputStream, "UTF-8"); + if (inputSpecificPlaylist == null) { + throw new Exception("Unsupported playlist " + fileName); + } + PlaylistImportHandler importHandler = getImportHandler(inputSpecificPlaylist); + LOG.debug("Using {} playlist import handler", importHandler.getClass().getSimpleName()); + + Pair, List> result = importHandler.handle(inputSpecificPlaylist, file); + + if (result.getLeft().isEmpty() && !result.getRight().isEmpty()) { + throw new Exception("No songs in the playlist were found."); + } + + for (String error : result.getRight()) { + LOG.warn("File in playlist '{}' not found: {}", fileName, error); + } + if (existingPlaylist == null) { + Playlist playlist = new Playlist(); + playlist.setUsername(user.getUsername()); + playlist.setShared(true); + playlist.setName(playlistName); + playlist.setComment("Auto-imported from " + fileName); + playlist.setImportedFrom(fileName); + Playlist savedPlaylist = playlistService.createPlaylist(playlist); + playlistService.broadcast(savedPlaylist); + savedPlaylist = playlistService.setFilesInPlaylist(savedPlaylist.getId(), result.getLeft()); + playlistService.broadcastFileChange(savedPlaylist.getId(), true, true); + return savedPlaylist; + } else { + playlistService.setFilesInPlaylist(existingPlaylist.getId(), result.getLeft()); + playlistService.broadcastFileChange(existingPlaylist.getId(), existingPlaylist.getShared(), true); + return existingPlaylist; + } + } + + private void importPlaylistIfUpdated(Path file, Playlist existingPlaylist) throws Exception { + String fileName = file.getFileName().toString(); + if (existingPlaylist != null && Files.getLastModifiedTime(file).toMillis() <= existingPlaylist.getChanged().toEpochMilli()) { + // Already imported and not changed since. + return; + } + + User sysAdmin = UserUtil.getSysAdmin(userRepository.findAll()); + if (sysAdmin == null) { + LOG.error("No admin user found, skipping auto-import of playlist {}", file); + return; + } + + try (InputStream in = Files.newInputStream(file)) { + importPlaylist(sysAdmin, FilenameUtils.getBaseName(fileName), fileName, null, in, existingPlaylist); + LOG.info("Auto-imported playlist {}", file); + } + } + + public String getExportPlaylistExtension() { + String format = settingsService.getPlaylistExportFormat(); + SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format); + return provider.getContentTypes()[0].getExtensions()[0]; + } + + public void exportPlaylist(int id, OutputStream out) throws Exception { + String format = settingsService.getPlaylistExportFormat(); + SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format); + PlaylistExportHandler handler = getExportHandler(provider); + SpecificPlaylist specificPlaylist = handler.handle(id, provider); + specificPlaylist.writeTo(out, StringUtil.ENCODING_UTF8); + } + + private PlaylistImportHandler getImportHandler(SpecificPlaylist playlist) { + return importHandlers.stream() + .filter(handler -> handler.canHandle(playlist.getClass())) + .findFirst() + .orElseThrow(() -> new RuntimeException("No import handler for " + playlist.getClass().getName())); + + } + + private PlaylistExportHandler getExportHandler(SpecificPlaylistProvider provider) { + return exportHandlers.stream() + .filter(handler -> handler.canHandle(provider.getClass())) + .findFirst() + .orElseThrow(() -> new RuntimeException("No export handler for " + provider.getClass().getName())); + } +} \ No newline at end of file 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 c146fa730..3073234bb 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 @@ -19,44 +19,28 @@ */ package org.airsonic.player.service; -import chameleon.playlist.SpecificPlaylist; -import chameleon.playlist.SpecificPlaylistFactory; -import chameleon.playlist.SpecificPlaylistProvider; -import org.airsonic.player.dao.MediaFileDao; -import org.airsonic.player.dao.PlaylistDao; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.PlayQueue; import org.airsonic.player.domain.Playlist; import org.airsonic.player.domain.User; -import org.airsonic.player.service.playlist.PlaylistExportHandler; -import org.airsonic.player.service.playlist.PlaylistImportHandler; -import org.airsonic.player.util.StringUtil; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.tuple.Pair; +import org.airsonic.player.repository.PlaylistRepository; +import org.airsonic.player.repository.UserRepository; +import org.airsonic.player.service.websocket.AsyncWebSocketClient; +import org.airsonic.player.util.LambdaUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import javax.annotation.PostConstruct; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.WatchEvent; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.concurrent.CompletableFuture.runAsync; - /** * Provides services for loading and saving playlists to and from persistent storage. * @@ -64,139 +48,225 @@ * @see PlayQueue */ @Service +@Transactional public class PlaylistService { private static final Logger LOG = LoggerFactory.getLogger(PlaylistService.class); - private final MediaFileDao mediaFileDao; - private final PlaylistDao playlistDao; - private final UserService userService; - private final SecurityService securityService; - private final SettingsService settingsService; - private final List exportHandlers; - private final List importHandlers; - private final SimpMessagingTemplate brokerTemplate; - private final PathWatcherService pathWatcherService; + private final UserRepository userRepository; + private final PlaylistRepository playlistRepository; + private final AsyncWebSocketClient asyncWebSocketClient; @Autowired - public PlaylistService(MediaFileDao mediaFileDao, - PlaylistDao playlistDao, - UserService userService, - SecurityService securityService, - SettingsService settingsService, - List exportHandlers, - List importHandlers, - SimpMessagingTemplate brokerTemplate, - PathWatcherService pathWatcherService) { - - this.mediaFileDao = mediaFileDao; - this.playlistDao = playlistDao; - this.userService = userService; - this.securityService = securityService; - this.settingsService = settingsService; - this.exportHandlers = exportHandlers; - this.importHandlers = importHandlers; - this.brokerTemplate = brokerTemplate; - this.pathWatcherService = pathWatcherService; - } - - @PostConstruct - public void init() throws IOException { - addPlaylistFolderWatcher(); - } + public PlaylistService(UserRepository userRepository, + PlaylistRepository playlistRepository, + AsyncWebSocketClient asyncWebSocketClient) { - public void addPlaylistFolderWatcher() { - Path playlistFolder = Paths.get(settingsService.getPlaylistFolder()); - if (Files.exists(playlistFolder) && Files.isDirectory(playlistFolder)) { - try { - pathWatcherService.setWatcher("Playlist folder watcher", playlistFolder, - this::handleModifiedPlaylist, null, this::handleModifiedPlaylist, null); - } catch (Exception e) { - LOG.warn("Issues setting watcher for folder: {}", playlistFolder); - } - } - } - - private void handleModifiedPlaylist(Path path, WatchEvent event) { - Path fullPath = path.resolve(event.context()); - importPlaylist(fullPath, playlistDao.getAllPlaylists()); + this.userRepository = userRepository; + this.playlistRepository = playlistRepository; + this.asyncWebSocketClient = asyncWebSocketClient; } + /** + * Returns all playlists. + * + * @return All playlists. + */ public List getAllPlaylists() { - return playlistDao.getAllPlaylists(); + Sort sort = Sort.by("name").ascending(); + return playlistRepository.findAll(sort); } + + /** + * Returns all playlists that the given user is allowed to read. + * + * @param username The user. If {@code null}, no playlists are returned. + * @return All playlists that the given user is allowed to read. + */ public List getReadablePlaylistsForUser(String username) { - return playlistDao.getReadablePlaylistsForUser(username); + + if (username == null) { + return Collections.emptyList(); + } + + List result1 = playlistRepository.findByUsername(username); + List result2 = playlistRepository.findBySharedTrue(); + List result3 = playlistRepository.findByUsernameNotAndSharedUsersUsername(username, username); + + // Remove duplicates. + return Stream.of(result1, result2, result3) + .flatMap(r -> r.parallelStream()) + .filter(LambdaUtils.distinctByKey(p -> p.getId())) + .sorted(Comparator.comparing(Playlist::getName)) + .toList(); } + /** + * Returns all playlists that the given user is allowed to write. + * + * @param username The user. If {@code null}, no playlists are returned. + * @return + */ public List getWritablePlaylistsForUser(String username) { + return userRepository.findByUsername(username).map(user -> { + if (user.isAdminRole()) { + return getReadablePlaylistsForUser(username); + } else { + return playlistRepository.findByUsernameOrderByNameAsc(username); + } + }).orElseGet(() -> { + LOG.warn("User {} not found", username); + return new ArrayList<>(); + }); - // Admin users are allowed to modify all playlists that are visible to them. - if (securityService.isAdmin(username)) { - return getReadablePlaylistsForUser(username); - } - - return playlistDao.getWritablePlaylistsForUser(username); } @Cacheable(cacheNames = "playlistCache", unless = "#result == null") public Playlist getPlaylist(int id) { - return playlistDao.getPlaylist(id); + return playlistRepository.findById(id).orElse(null); } @Cacheable(cacheNames = "playlistUsersCache", unless = "#result == null") public List getPlaylistUsers(int playlistId) { - return playlistDao.getPlaylistUsers(playlistId); + List users = playlistRepository.findById(playlistId).map(Playlist::getSharedUsers).orElse(Collections.emptyList()); + return users.stream().map(User::getUsername).filter(Objects::nonNull).toList(); } public List getFilesInPlaylist(int id) { return getFilesInPlaylist(id, false); } + + @Transactional(readOnly = true) public List getFilesInPlaylist(int id, boolean includeNotPresent) { - return mediaFileDao.getFilesInPlaylist(id).stream().filter(x -> includeNotPresent || x.isPresent()).collect(Collectors.toList()); + return playlistRepository.findById(id).map(p -> p.getMediaFiles()).orElseGet( + () -> { + LOG.warn("Playlist {} not found", id); + return new ArrayList<>(); + } + ).stream().filter(x -> includeNotPresent || x.isPresent()).collect(Collectors.toList()); + } + + public Playlist setFilesInPlaylist(int id, List files) { + return playlistRepository.findById(id).map(p -> setFilesInPlaylist(p, files)).orElseGet( + () -> { + LOG.warn("Playlist {} not found", id); + return null; + }); } - public void setFilesInPlaylist(int id, List files) { - playlistDao.setFilesInPlaylist(id, files); - refreshPlaylistStats(id); + private Playlist setFilesInPlaylist(Playlist playlist, List files) { + playlist.setMediaFiles(files); + playlist.setFileCount(files.size()); + playlist.setDuration(files.stream().mapToDouble(MediaFile::getDuration).sum()); + playlist.setChanged(Instant.now()); + playlistRepository.saveAndFlush(playlist); + return playlist; } - public void refreshPlaylistsStats() { - getAllPlaylists().forEach(p -> refreshPlaylistStats(p.getId())); + @CacheEvict(cacheNames = "playlistCache", key = "#id") + public void removeFilesInPlaylistByIndices(int id, List indices) { + playlistRepository.findById(id).ifPresentOrElse(p -> { + List files = p.getMediaFiles(); + List newFiles = new ArrayList<>(); + for (int i = 0; i < files.size(); i++) { + if (!indices.contains(i)) { + newFiles.add(files.get(i)); + } + } + setFilesInPlaylist(p, newFiles); + }, () -> { + LOG.warn("Playlist {} not found", id); + } + ); } - public void refreshPlaylistStats(int id) { - Playlist playlist = new Playlist(getPlaylist(id)); - Pair stats = playlistDao.getPlaylistFileStats(id); - playlist.setFileCount(stats.getLeft()); - playlist.setDuration(stats.getRight()); - updatePlaylist(playlist, true); + /** + * Refreshes the file count and duration of all playlists. + */ + public List refreshPlaylistsStats() { + return playlistRepository.findAll().stream().map(p -> { + p.setFileCount(p.getMediaFiles().size()); + p.setDuration(p.getMediaFiles().stream().mapToDouble(MediaFile::getDuration).sum()); + p.setChanged(Instant.now()); + playlistRepository.save(p); + return p; + }).collect(Collectors.toList()); } - public void createPlaylist(Playlist playlist) { - playlistDao.createPlaylist(playlist); + /** + * Creates a new playlist. + * + * @param name the playlist name + * @param shared if true, the playlist is shared with other users + * @param username the username of the user that created the playlist + * @return the created playlist + */ + public Playlist createPlaylist(String name, boolean shared, String username) { + Instant now = Instant.now(); + Playlist playlist = new Playlist(); + playlist.setName(name); + playlist.setShared(shared); + playlist.setUsername(username); + playlist.setCreated(now); + playlist.setChanged(now); + playlistRepository.save(playlist); + return playlist; + } + + + /** + * Creates a new playlist. + * @param playlist + */ + public Playlist createPlaylist(Playlist playlist) { + Instant now = Instant.now(); + playlist.setCreated(now); + playlist.setChanged(now); + playlistRepository.save(playlist); if (playlist.getShared()) { - runAsync(() -> brokerTemplate.convertAndSend("/topic/playlists/updated", playlist)); + asyncWebSocketClient.send("/topic/playlists/updated", playlist); } else { - runAsync(() -> brokerTemplate.convertAndSendToUser(playlist.getUsername(), "/queue/playlists/updated", playlist)); + asyncWebSocketClient.sendToUser(playlist.getUsername(), "/queue/playlists/updated", playlist); } + return playlist; } @CacheEvict(cacheNames = "playlistUsersCache", key = "#playlist.id") public void addPlaylistUser(Playlist playlist, String username) { - playlistDao.addPlaylistUser(playlist.getId(), username); - // this might cause dual notifications on the client if the playlist is already public - runAsync(() -> brokerTemplate.convertAndSendToUser(username, "/queue/playlists/updated", playlist)); + userRepository.findByUsername(username).ifPresentOrElse(user -> { + playlistRepository.findById(playlist.getId()).ifPresentOrElse(p -> { + if (!p.getSharedUsers().contains(user)) { + p.addSharedUser(user); + playlistRepository.save(p); + // this might cause dual notifications on the client if the playlist is already public + asyncWebSocketClient.sendToUser(username, "/queue/playlists/updated", p); + } else { + LOG.info("Playlist {} already shared with {}", playlist.getId(), username); + } + }, () -> { + LOG.warn("Playlist {} not found", playlist.getId()); + } + ); + }, () -> { + LOG.warn("User {} not found", username); + } + ); } @CacheEvict(cacheNames = "playlistUsersCache", key = "#playlist.id") public void deletePlaylistUser(Playlist playlist, String username) { - playlistDao.deletePlaylistUser(playlist.getId(), username); - if (!playlist.getShared()) { - runAsync(() -> brokerTemplate.convertAndSendToUser(username, "/queue/playlists/deleted", playlist.getId())); - } + playlistRepository.findByIdAndSharedUsersUsername(playlist.getId(), username).ifPresentOrElse(p -> { + p.removeSharedUserByUsername(username); + playlistRepository.save(p); + if (!p.getShared()) { + asyncWebSocketClient.sendToUser(username, "/queue/playlists/deleted", p.getId()); + } + }, () -> { + LOG.warn("Playlist {} shared with {} not found", playlist.getId(), username); + } + ); } public boolean isReadAllowed(Playlist playlist, String username) { @@ -209,38 +279,91 @@ public boolean isReadAllowed(Playlist playlist, String username) { return getPlaylistUsers(playlist.getId()).contains(username); } + /** + * Returns true if the playlist exists. + * + * @param id the playlist id + * @return true if the playlist exists + */ + @Transactional(readOnly = true) + public boolean isExist(Integer id) { + return id != null && playlistRepository.existsById(id); + } + + public boolean isWriteAllowed(Integer id, String username) { + return username != null && playlistRepository.existsByIdAndUsername(id, username); + } + public boolean isWriteAllowed(Playlist playlist, String username) { return username != null && username.equals(playlist.getUsername()); } @CacheEvict(cacheNames = "playlistCache") public void deletePlaylist(int id) { - playlistDao.deletePlaylist(id); - runAsync(() -> brokerTemplate.convertAndSend("/topic/playlists/deleted", id)); + playlistRepository.deleteById(id); + asyncWebSocketClient.send("/topic/playlists/deleted", id); + } + + + /** + * Broadcasts the playlist to all users that have access to it. + * + * @param id the playlist id to broadcast + */ + public void broadcastDeleted(int id) { + asyncWebSocketClient.send("/topic/playlists/deleted", id); } - public void updatePlaylist(Playlist playlist) { - updatePlaylist(playlist, false); + public void updatePlaylist(Integer id, String name) { + updatePlaylist(id, name, null, null); + } + + @CacheEvict(cacheNames = "playlistCache", key = "#id") + public void updatePlaylist(Integer id, String name, String comment, Boolean shared) { + playlistRepository.findById(id).ifPresentOrElse(p -> { + p.setName(name); + if (comment != null) p.setComment(comment); + if (shared != null) p.setShared(shared); + p.setChanged(Instant.now()); + playlistRepository.save(p); + }, () -> { + LOG.warn("Playlist {} not found", id); + } + ); } /** - * DO NOT pass in the mutated cache value. This method relies on the existing - * cached value to check the differences + * Broadcasts the playlist to all users that have access to it. + * + * @param playlist the playlist to broadcast */ - @CacheEvict(cacheNames = "playlistCache", key = "#playlist.id") - public void updatePlaylist(Playlist playlist, boolean filesChangedBroadcastContext) { - Playlist oldPlaylist = getPlaylist(playlist.getId()); - playlistDao.updatePlaylist(playlist); - runAsync(() -> { + public void broadcast(Playlist playlist) { + if (playlist.getShared()) { + asyncWebSocketClient.send("/topic/playlists/updated", playlist); + } else { + asyncWebSocketClient.sendToUser(playlist.getUsername(), "/queue/playlists/updated", playlist); + } + } + + + /** + * Broadcasts the playlist to all users that have access to it. + * + * @param id the playlist id to broadcast + * @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 + */ + public void broadcastFileChange(Integer id, boolean isShared, boolean filesChangedBroadcastContext) { + playlistRepository.findById(id).ifPresent(playlist -> { BroadcastedPlaylist bp = new BroadcastedPlaylist(playlist, filesChangedBroadcastContext); if (playlist.getShared()) { - brokerTemplate.convertAndSend("/topic/playlists/updated", bp); + asyncWebSocketClient.send("/topic/playlists/updated", bp); } else { - if (oldPlaylist.getShared()) { - brokerTemplate.convertAndSend("/topic/playlists/deleted", playlist.getId()); + if (isShared) { + asyncWebSocketClient.send("/topic/playlists/deleted", playlist.getId()); } - Stream.concat(Stream.of(playlist.getUsername()), getPlaylistUsers(playlist.getId()).stream()) - .forEach(u -> brokerTemplate.convertAndSendToUser(u, "/queue/playlists/updated", bp)); + Stream.concat(Stream.of(playlist.getUsername()), playlist.getSharedUsers().stream().map(User::getUsername)) + .forEach(u -> asyncWebSocketClient.sendToUser(u, "/queue/playlists/updated", bp)); } }); } @@ -250,6 +373,7 @@ public static class BroadcastedPlaylist extends Playlist { public BroadcastedPlaylist(Playlist p, boolean filesChanged) { super(p); + this.setId(p.getId()); this.filesChanged = filesChanged; } @@ -258,127 +382,5 @@ public boolean getFilesChanged() { } } - public String getExportPlaylistExtension() { - String format = settingsService.getPlaylistExportFormat(); - SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format); - return provider.getContentTypes()[0].getExtensions()[0]; - } - - public void exportPlaylist(int id, OutputStream out) throws Exception { - String format = settingsService.getPlaylistExportFormat(); - SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format); - PlaylistExportHandler handler = getExportHandler(provider); - SpecificPlaylist specificPlaylist = handler.handle(id, provider); - specificPlaylist.writeTo(out, StringUtil.ENCODING_UTF8); - } - - private PlaylistImportHandler getImportHandler(SpecificPlaylist playlist) { - return importHandlers.stream() - .filter(handler -> handler.canHandle(playlist.getClass())) - .findFirst() - .orElseThrow(() -> new RuntimeException("No import handler for " + playlist.getClass().getName())); - - } - - private PlaylistExportHandler getExportHandler(SpecificPlaylistProvider provider) { - return exportHandlers.stream() - .filter(handler -> handler.canHandle(provider.getClass())) - .findFirst() - .orElseThrow(() -> new RuntimeException("No export handler for " + provider.getClass().getName())); - } - public void importPlaylists() { - try { - LOG.info("Starting playlist import."); - doImportPlaylists(); - LOG.info("Completed playlist import."); - } catch (Throwable x) { - LOG.warn("Failed to import playlists: " + x, x); - } - } - - private void doImportPlaylists() { - String playlistFolderPath = settingsService.getPlaylistFolder(); - if (playlistFolderPath == null) { - return; - } - Path playlistFolder = Paths.get(playlistFolderPath); - if (!Files.exists(playlistFolder)) { - return; - } - - List allPlaylists = playlistDao.getAllPlaylists(); - try (Stream children = Files.walk(playlistFolder)) { - children.forEach(f -> importPlaylist(f, allPlaylists)); - } catch (IOException ex) { - LOG.warn("Error while reading directory {} when importing playlists", playlistFolder, ex); - } - } - - private void importPlaylist(Path f, List allPlaylists) { - if (Files.isRegularFile(f) && Files.isReadable(f)) { - try { - Playlist playlist = allPlaylists.stream() - .filter(p -> f.getFileName().toString().equals(p.getImportedFrom())) - .findAny().orElse(null); - importPlaylistIfUpdated(f, playlist); - } catch (Exception x) { - LOG.warn("Failed to auto-import playlist {}", f, x); - } - } - } - - public Playlist importPlaylist(String username, String playlistName, String fileName, Path file, InputStream inputStream, Playlist existingPlaylist) throws Exception { - - // TODO: handle other encodings - final SpecificPlaylist inputSpecificPlaylist = SpecificPlaylistFactory.getInstance().readFrom(inputStream, "UTF-8"); - if (inputSpecificPlaylist == null) { - throw new Exception("Unsupported playlist " + fileName); - } - PlaylistImportHandler importHandler = getImportHandler(inputSpecificPlaylist); - LOG.debug("Using {} playlist import handler", importHandler.getClass().getSimpleName()); - - Pair, List> result = importHandler.handle(inputSpecificPlaylist, file); - - if (result.getLeft().isEmpty() && !result.getRight().isEmpty()) { - throw new Exception("No songs in the playlist were found."); - } - - for (String error : result.getRight()) { - LOG.warn("File in playlist '{}' not found: {}", fileName, error); - } - Instant now = Instant.now(); - Playlist playlist; - if (existingPlaylist == null) { - playlist = new Playlist(); - playlist.setUsername(username); - playlist.setCreated(now); - playlist.setChanged(now); - playlist.setShared(true); - playlist.setName(playlistName); - playlist.setComment("Auto-imported from " + fileName); - playlist.setImportedFrom(fileName); - createPlaylist(playlist); - } else { - playlist = existingPlaylist; - } - - setFilesInPlaylist(playlist.getId(), result.getLeft()); - - return playlist; - } - - private void importPlaylistIfUpdated(Path file, Playlist existingPlaylist) throws Exception { - String fileName = file.getFileName().toString(); - if (existingPlaylist != null && Files.getLastModifiedTime(file).toMillis() <= existingPlaylist.getChanged().toEpochMilli()) { - // Already imported and not changed since. - return; - } - - User sysAdmin = userService.getSysAdmin(); - try (InputStream in = Files.newInputStream(file)) { - importPlaylist(sysAdmin.getUsername(), FilenameUtils.getBaseName(fileName), fileName, null, in, existingPlaylist); - LOG.info("Auto-imported playlist {}", file); - } - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SonosService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SonosService.java index 3def1b859..211434626 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SonosService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SonosService.java @@ -360,15 +360,8 @@ public void getMediaURI(String id, MediaUriAction action, Integer secondsSinceEx @Override public CreateContainerResult createContainer(String containerType, String title, String parentId, String seedId) { - Instant now = Instant.now(); - Playlist playlist = new Playlist(); - playlist.setName(title); - playlist.setUsername(getUsername()); - playlist.setCreated(now); - playlist.setChanged(now); - playlist.setShared(false); - - playlistService.createPlaylist(playlist); + Playlist playlist = playlistService.createPlaylist(title, false, getUsername()); + playlistService.broadcast(playlist); CreateContainerResult result = new CreateContainerResult(); result.setId(ID_PLAYLIST_PREFIX + playlist.getId()); addItemToPlaylist(playlist.getId(), seedId, -1); @@ -380,9 +373,9 @@ public CreateContainerResult createContainer(String containerType, String title, public DeleteContainerResult deleteContainer(String id) { if (id.startsWith(ID_PLAYLIST_PREFIX)) { int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); - Playlist playlist = playlistService.getPlaylist(playlistId); - if (playlist != null && playlist.getUsername().equals(getUsername())) { + if (playlistService.isWriteAllowed(playlistId, getUsername())) { playlistService.deletePlaylist(playlistId); + playlistService.broadcastDeleted(playlistId); } } return new DeleteContainerResult(); @@ -392,12 +385,10 @@ public DeleteContainerResult deleteContainer(String id) { public RenameContainerResult renameContainer(String id, String title) { if (id.startsWith(ID_PLAYLIST_PREFIX)) { int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); - Playlist playlist = playlistService.getPlaylist(playlistId); - if (playlist != null && playlist.getUsername().equals(getUsername())) { + if (playlistService.isWriteAllowed(playlistId, getUsername())) { // create a copy to update - playlist = new Playlist(playlist); - playlist.setName(title); - playlistService.updatePlaylist(playlist); + playlistService.updatePlaylist(playlistId, title); + playlistService.broadcastFileChange(playlistId, false, false); } } return new RenameContainerResult(); @@ -407,8 +398,7 @@ public RenameContainerResult renameContainer(String id, String title) { public AddToContainerResult addToContainer(String id, String parentId, int index, String updateId) { if (parentId.startsWith(ID_PLAYLIST_PREFIX)) { int playlistId = Integer.parseInt(parentId.replace(ID_PLAYLIST_PREFIX, "")); - Playlist playlist = playlistService.getPlaylist(playlistId); - if (playlist != null && playlist.getUsername().equals(getUsername())) { + if (playlistService.isWriteAllowed(playlistId, getUsername())) { addItemToPlaylist(playlistId, id, index); } } @@ -442,14 +432,14 @@ private void addItemToPlaylist(int playlistId, String id, int index) { existingSongs.addAll(index, newSongs); playlistService.setFilesInPlaylist(playlistId, existingSongs); + playlistService.broadcastFileChange(playlistId, false, true); } @Override public ReorderContainerResult reorderContainer(String id, String from, int to, String updateId) { if (id.startsWith(ID_PLAYLIST_PREFIX)) { int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); - Playlist playlist = playlistService.getPlaylist(playlistId); - if (playlist != null && playlist.getUsername().equals(getUsername())) { + if (playlistService.isWriteAllowed(playlistId, getUsername())) { SortedMap indexToSong = new ConcurrentSkipListMap(); List songs = playlistService.getFilesInPlaylist(playlistId); @@ -468,6 +458,7 @@ public ReorderContainerResult reorderContainer(String id, String from, int to, S updatedSongs.addAll(indexToSong.tailMap(to).values()); playlistService.setFilesInPlaylist(playlistId, updatedSongs); + playlistService.broadcastFileChange(playlistId, false, true); } } return new ReorderContainerResult(); @@ -477,8 +468,7 @@ public ReorderContainerResult reorderContainer(String id, String from, int to, S public RemoveFromContainerResult removeFromContainer(String id, String indices, String updateId) { if (id.startsWith(ID_PLAYLIST_PREFIX)) { int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); - Playlist playlist = playlistService.getPlaylist(playlistId); - if (playlist != null && playlist.getUsername().equals(getUsername())) { + if (playlistService.isWriteAllowed(playlistId, getUsername())) { SortedSet indicesToRemove = parsePlaylistIndices(indices); List songs = playlistService.getFilesInPlaylist(playlistId); List updatedSongs = new ArrayList(); @@ -488,6 +478,7 @@ public RemoveFromContainerResult removeFromContainer(String id, String indices, } } playlistService.setFilesInPlaylist(playlistId, updatedSongs); + playlistService.broadcastFileChange(playlistId, false, true); } } return new RemoveFromContainerResult(); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/playlist/DefaultPlaylistExportHandler.java b/airsonic-main/src/main/java/org/airsonic/player/service/playlist/DefaultPlaylistExportHandler.java index e518c310c..89f74c14a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/playlist/DefaultPlaylistExportHandler.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/playlist/DefaultPlaylistExportHandler.java @@ -5,22 +5,24 @@ import chameleon.playlist.Playlist; import chameleon.playlist.SpecificPlaylist; import chameleon.playlist.SpecificPlaylistProvider; -import org.airsonic.player.dao.MediaFileDao; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.repository.PlaylistRepository; import org.airsonic.player.service.MediaFolderService; import org.airsonic.player.util.StringUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Component public class DefaultPlaylistExportHandler implements PlaylistExportHandler { @Autowired - MediaFileDao mediaFileDao; + private PlaylistRepository playlistRepository; @Autowired private MediaFolderService mediaFolderService; @@ -31,14 +33,19 @@ public boolean canHandle(Class providerClass } @Override + @Transactional public SpecificPlaylist handle(int id, SpecificPlaylistProvider provider) throws Exception { chameleon.playlist.Playlist playlist = createChameleonGenericPlaylistFromDBId(id); return provider.toSpecificPlaylist(playlist); } - Playlist createChameleonGenericPlaylistFromDBId(int id) { + private Playlist createChameleonGenericPlaylistFromDBId(int id) { Playlist newPlaylist = new Playlist(); - List files = mediaFileDao.getFilesInPlaylist(id); + List files = playlistRepository.findById(id).map(playlist -> { + return playlist.getMediaFiles(); + }).orElseGet(() -> { + return new ArrayList<>(); + }); files.forEach(file -> { MusicFolder folder = mediaFolderService.getMusicFolderById(file.getFolderId()); Media component = new Media(); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/playlist/XspfPlaylistExportHandler.java b/airsonic-main/src/main/java/org/airsonic/player/service/playlist/XspfPlaylistExportHandler.java index 0f330d365..a395d8a75 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/playlist/XspfPlaylistExportHandler.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/playlist/XspfPlaylistExportHandler.java @@ -5,30 +5,29 @@ import chameleon.playlist.xspf.Location; import chameleon.playlist.xspf.Track; import chameleon.playlist.xspf.XspfProvider; -import org.airsonic.player.dao.MediaFileDao; -import org.airsonic.player.dao.PlaylistDao; import org.airsonic.player.domain.CoverArt.EntityType; -import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.domain.Playlist; +import org.airsonic.player.repository.PlaylistRepository; import org.airsonic.player.service.CoverArtService; import org.airsonic.player.service.MediaFolderService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.Date; -import java.util.List; import java.util.Optional; @Component public class XspfPlaylistExportHandler implements PlaylistExportHandler { - @Autowired - MediaFileDao mediaFileDao; + private static Logger LOG = LoggerFactory.getLogger(XspfPlaylistExportHandler.class); @Autowired - PlaylistDao playlistDao; + PlaylistRepository playlistRepository; @Autowired MediaFolderService mediaFolderService; @@ -42,19 +41,22 @@ public boolean canHandle(Class providerClass } @Override + @Transactional public SpecificPlaylist handle(int id, SpecificPlaylistProvider provider) { return createXsfpPlaylistFromDBId(id); } chameleon.playlist.xspf.Playlist createXsfpPlaylistFromDBId(int id) { chameleon.playlist.xspf.Playlist newPlaylist = new chameleon.playlist.xspf.Playlist(); - Playlist playlist = playlistDao.getPlaylist(id); + Playlist playlist = playlistRepository.findById(id).orElseGet(() -> { + LOG.error("Playlist with id {} not found", id); + return null; + }); newPlaylist.setTitle(playlist.getName()); newPlaylist.setCreator("Airsonic user " + playlist.getUsername()); newPlaylist.setDate(Date.from(Instant.now())); //TODO switch to Instant upstream - List files = mediaFileDao.getFilesInPlaylist(id); - files.stream().map(mediaFile -> { + playlist.getMediaFiles().stream().map(mediaFile -> { MusicFolder folder = mediaFolderService.getMusicFolderById(mediaFile.getFolderId()); Track track = new Track(); track.setTrackNumber(mediaFile.getTrackNumber()); diff --git a/airsonic-main/src/main/java/org/airsonic/player/util/UserUtil.java b/airsonic-main/src/main/java/org/airsonic/player/util/UserUtil.java new file mode 100644 index 000000000..78414391e --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/util/UserUtil.java @@ -0,0 +1,36 @@ +package org.airsonic.player.util; + +import org.airsonic.player.domain.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +public class UserUtil { + + private final static Logger LOG = LoggerFactory.getLogger(UserUtil.class); + + /** + * Get system administrator user, which system operations perform on behalf. + * + * @param users all users. + * @return system admin. + */ + public static User getSysAdmin(List users) { + LOG.debug("getSysAdmin"); + if (CollectionUtils.isEmpty(users)) { + LOG.debug("getSysAdmin: users is empty"); + return null; + } + + List admins = users.stream() + .filter(User::isAdminRole).toList(); + + return admins.stream() + .filter(user -> User.USERNAME_ADMIN.equals(user.getUsername())) + .findAny() + .orElseGet(() -> admins.iterator().next()); + } + +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/controller/CoverArtControllerTest.java b/airsonic-main/src/test/java/org/airsonic/player/controller/CoverArtControllerTest.java index 1d0404116..b6cfa66ee 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/controller/CoverArtControllerTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/controller/CoverArtControllerTest.java @@ -18,7 +18,6 @@ */ package org.airsonic.player.controller; -import org.airsonic.player.dao.PlaylistDao; import org.airsonic.player.domain.Album; import org.airsonic.player.domain.Artist; import org.airsonic.player.domain.CoverArt; @@ -31,6 +30,7 @@ import org.airsonic.player.service.ArtistService; import org.airsonic.player.service.CoverArtService; import org.airsonic.player.service.MediaFileService; +import org.airsonic.player.service.PlaylistService; import org.airsonic.player.service.PodcastService; import org.airsonic.player.util.ImageUtil; import org.junit.jupiter.api.BeforeAll; @@ -86,7 +86,7 @@ public class CoverArtControllerTest { private ArtistService artistService; @MockBean - private PlaylistDao playlistDao; + private PlaylistService playlistService; @MockBean private PodcastService podcastService; @@ -277,7 +277,7 @@ public void getCoverArtWithPlaylistIdTest() throws Exception { mockedPlaylist.setName("playlist"); // set up mock - when(playlistDao.getPlaylist(anyInt())).thenReturn(mockedPlaylist); + when(playlistService.getPlaylist(anyInt())).thenReturn(mockedPlaylist); // prepare expected byte[] expected = PLAYLIST_RESOURCE.getInputStream().readAllBytes(); diff --git a/airsonic-main/src/test/java/org/airsonic/player/controller/ImportPlaylistControllerTest.java b/airsonic-main/src/test/java/org/airsonic/player/controller/ImportPlaylistControllerTest.java index b553f99de..61f377077 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/controller/ImportPlaylistControllerTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/controller/ImportPlaylistControllerTest.java @@ -1,7 +1,8 @@ package org.airsonic.player.controller; import org.airsonic.player.domain.Playlist; -import org.airsonic.player.service.PlaylistService; +import org.airsonic.player.domain.User; +import org.airsonic.player.service.PlaylistFileService; import org.airsonic.player.service.SecurityService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -57,7 +58,7 @@ public class ImportPlaylistControllerTest { private SecurityService securityService; @MockBean - private PlaylistService playlistService; + private PlaylistFileService playlistFileService; @TempDir private static Path tempDir; @@ -87,8 +88,11 @@ public void handlePostWithValidFileShouldImportPlaylistAndRedirect() throws Exce expectedPlaylist.setName(PLAYLIST_NAME); expectedPlaylist.setUsername(USERNAME); - when(securityService.getCurrentUsername(any())).thenReturn(USERNAME); - when(playlistService.importPlaylist(eq(USERNAME), eq(PLAYLIST_NAME), eq(FILE_NAME), isNull(), any(), isNull())).thenReturn(expectedPlaylist); + User user = new User(); + user.setUsername(USERNAME); + + when(securityService.getCurrentUser(any())).thenReturn(user); + when(playlistFileService.importPlaylist(eq(user), eq(PLAYLIST_NAME), eq(FILE_NAME), isNull(), any(), isNull())).thenReturn(expectedPlaylist); MvcResult result = mockMvc.perform(request) .andExpect(status().is3xxRedirection()) @@ -121,7 +125,7 @@ public void handlePostWithBlankFileNameShouldNotImportPlaylistAndRedirectWithErr Map actual = (Map) result.getFlashMap().get("model"); assertNull(actual.get("playlist")); assertEquals("No file specified.", actual.get("error")); - verify(playlistService, never()).importPlaylist(anyString(), anyString(), anyString(), isNull(), any(), isNull()); + verify(playlistFileService, never()).importPlaylist(any(User.class), anyString(), anyString(), isNull(), any(), isNull()); } @@ -145,7 +149,7 @@ public void handlePostWithLargeFileShouldNotImportPlaylistAndRedirectWithError() Map actual = (Map) result.getFlashMap().get("model"); assertNull(actual.get("playlist")); assertEquals("The playlist file is too large. Max file size is 5 MB.", actual.get("error")); - verify(playlistService, never()).importPlaylist(anyString(), anyString(), anyString(), isNull(), any(), isNull()); + verify(playlistFileService, never()).importPlaylist(any(User.class), anyString(), anyString(), isNull(), any(), isNull()); } } 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 688432cab..b412aad9c 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 @@ -21,7 +21,7 @@ public class MediaScannerServiceUnitTest { @Mock private SettingsService settingsService; @Mock - private PlaylistService playlistService; + private PlaylistFileService playlistFileService; @Mock private MediaFileService mediaFileService; @Mock @@ -49,7 +49,7 @@ public void neverScanned() { when(settingsService.getIndexCreationInterval()).thenReturn(-1); when(settingsService.getIndexCreationHour()).thenReturn(-1); when(indexManager.getStatistics()).thenReturn(null); - MediaScannerService mediaScannerService = new MediaScannerService(settingsService, indexManager, playlistService, mediaFileService, mediaFolderService, coverArtService, mediaFileDao, artistRepository, albumRepository, taskService, messagingTemplate, scanConfig); + MediaScannerService mediaScannerService = new MediaScannerService(settingsService, indexManager, playlistFileService, mediaFileService, mediaFolderService, coverArtService, mediaFileDao, artistRepository, albumRepository, taskService, messagingTemplate, scanConfig); assertTrue(mediaScannerService.neverScanned()); when(indexManager.getStatistics()).thenReturn(new MediaLibraryStatistics()); diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestExport.java b/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestExport.java similarity index 61% rename from airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestExport.java rename to airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestExport.java index 3db2f33a8..192ad8146 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestExport.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestExport.java @@ -1,66 +1,62 @@ package org.airsonic.player.service; -import com.google.common.collect.Lists; -import org.airsonic.player.dao.MediaFileDao; -import org.airsonic.player.dao.PlaylistDao; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.domain.Playlist; +import org.airsonic.player.repository.PlaylistRepository; +import org.airsonic.player.repository.UserRepository; import org.airsonic.player.service.playlist.DefaultPlaylistExportHandler; +import org.airsonic.player.service.websocket.AsyncWebSocketClient; import org.apache.commons.io.output.ByteArrayOutputStream; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.mockito.junit.jupiter.MockitoExtension; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) -public class PlaylistServiceTestExport { +@ExtendWith(MockitoExtension.class) +public class PlaylistFileServiceTestExport { - PlaylistService playlistService; + PlaylistFileService playlistFileService; @InjectMocks DefaultPlaylistExportHandler defaultPlaylistExportHandler; @Mock - MediaFileDao mediaFileDao; + private PlaylistService playlistService; @Mock - PlaylistDao playlistDao; + private PlaylistRepository playlistRepository; @Mock - MediaFileService mediaFileService; + private MediaFileService mediaFileService; @Mock - MediaFolderService mediaFolderService; + private MediaFolderService mediaFolderService; @Mock - SettingsService settingsService; + private SettingsService settingsService; @Mock - private UserService userService; + private UserRepository userRepository; @Mock - SecurityService securityService; - - @Mock - private SimpMessagingTemplate brokerTemplate; + private AsyncWebSocketClient asyncWebSocketClient; @Mock private PathWatcherService pathWatcherService; @@ -68,8 +64,8 @@ public class PlaylistServiceTestExport { @Mock MusicFolder mockedFolder; - @Rule - public TemporaryFolder folder = new TemporaryFolder(); + @TempDir + private Path tempDir; @Captor ArgumentCaptor actual; @@ -77,30 +73,30 @@ public class PlaylistServiceTestExport { @Captor ArgumentCaptor> medias; - @Before + @BeforeEach public void setup() { - playlistService = new PlaylistService( - mediaFileDao, - playlistDao, - userService, - securityService, + playlistFileService = new PlaylistFileService( + playlistService, settingsService, - Lists.newArrayList(defaultPlaylistExportHandler), - Collections.emptyList(), - brokerTemplate, - pathWatcherService); + userRepository, + pathWatcherService, + new ArrayList<>(List.of(defaultPlaylistExportHandler)), + new ArrayList<>()); } @Test public void testExportToM3U() throws Exception { - when(mediaFileDao.getFilesInPlaylist(eq(23))).thenReturn(getPlaylistFiles()); + Playlist playlist = new Playlist(); + playlist.setId(23); + playlist.setMediaFiles(getPlaylistFiles()); + when(playlistRepository.findById(eq(23))).thenReturn(Optional.of(playlist)); when(settingsService.getPlaylistExportFormat()).thenReturn("m3u"); when(mediaFolderService.getMusicFolderById(any())).thenReturn(mockedFolder); when(mockedFolder.getPath()).thenReturn(Paths.get("/")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - playlistService.exportPlaylist(23, outputStream); + playlistFileService.exportPlaylist(23, outputStream); byte[] actual = outputStream.toByteArray(); byte[] expected = getClass().getResourceAsStream("/PLAYLISTS/23.m3u").readAllBytes(); assertArrayEquals(expected, actual); diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestImport.java b/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestImport.java new file mode 100644 index 000000000..25d75ca5f --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistFileServiceTestImport.java @@ -0,0 +1,277 @@ +package org.airsonic.player.service; + +import com.google.common.collect.Lists; +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.domain.Playlist; +import org.airsonic.player.domain.User; +import org.airsonic.player.repository.PlaylistRepository; +import org.airsonic.player.repository.UserRepository; +import org.airsonic.player.service.playlist.DefaultPlaylistImportHandler; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class PlaylistFileServiceTestImport { + + @InjectMocks + private DefaultPlaylistImportHandler defaultPlaylistImportHandler; + + @Mock + private PlaylistRepository playlistRepository; + + @Mock + private MediaFileService mediaFileService; + + @Mock + private SettingsService settingsService; + + @Mock + private UserRepository userRepository; + + @Mock + private SecurityService securityService; + + @Mock + private PathWatcherService pathWatcherService; + + @Mock + private PlaylistService playlistService; + + @TempDir + private Path tempDir; + + @Captor + private ArgumentCaptor playlistCaptor; + + @Captor + private ArgumentCaptor> medias; + + private PlaylistFileService playlistFileService; + + @BeforeEach + public void beforeEach() { + playlistFileService = new PlaylistFileService( + playlistService, + settingsService, + userRepository, + pathWatcherService, + Collections.emptyList(), + Lists.newArrayList(defaultPlaylistImportHandler)); + } + + @Test + public void testImportFromM3U() throws Exception { + // given + String username = "testUser"; + String playlistName = "test-playlist"; + StringBuilder builder = new StringBuilder(); + builder.append("#EXTM3U\n"); + File mf1 = tempDir.resolve("mf1.mp3").toFile(); + FileUtils.touch(mf1); + File mf2 = tempDir.resolve("mf2.mp3").toFile(); + FileUtils.touch(mf2); + File mf3 = tempDir.resolve("mf3.mp3").toFile(); + FileUtils.touch(mf3); + builder.append(mf1.getAbsolutePath()).append("\n"); + builder.append(mf2.getAbsolutePath()).append("\n"); + builder.append(mf3.getAbsolutePath()).append("\n"); + doAnswer(new PersistPlayList(23)).when(playlistService).createPlaylist(playlistCaptor.capture()); + when(playlistService.setFilesInPlaylist(eq(23), any())).thenAnswer((Answer) invocationOnMock -> { + Playlist playlist = playlistCaptor.getValue(); + playlist.setId(invocationOnMock.getArgument(0)); + List mediaFiles = invocationOnMock.getArgument(1); + playlist.setMediaFiles(mediaFiles); + playlist.setFileCount(mediaFiles.size()); + playlist.setDuration(mediaFiles.stream().mapToDouble(MediaFile::getDuration).sum()); + return playlist; + }); + doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); + InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); + String path = "/path/to/" + playlistName + ".m3u"; + User user = new User(); + user.setUsername(username); + + // when + Playlist actual = playlistFileService.importPlaylist(user, playlistName, path, Paths.get(path), inputStream, null); + + // then + verify(playlistService).createPlaylist(any(Playlist.class)); + verify(playlistService).setFilesInPlaylist(anyInt(), any()); + verify(playlistService).broadcast(any()); + verify(playlistService).broadcastFileChange(eq(23), eq(true), eq(true)); + verifyNoMoreInteractions(playlistService); + Playlist expected = new Playlist(); + expected.setUsername(username); + expected.setName(playlistName); + expected.setComment("Auto-imported from " + path); + expected.setImportedFrom(path); + expected.setShared(true); + expected.setId(23); + expected.setFileCount(3); + expected.setDuration(369.0); + assertTrue(EqualsBuilder.reflectionEquals(actual, expected, "created", "changed", "sharedUsers", "mediaFiles")); + assertEquals(3, actual.getMediaFiles().size()); + } + + @Test + public void testImportFromPLS() throws Exception { + String username = "testUser"; + String playlistName = "test-playlist"; + StringBuilder builder = new StringBuilder(); + builder.append("[playlist]\n"); + File mf1 = tempDir.resolve("mf1.mp3").toFile(); + FileUtils.touch(mf1); + File mf2 = tempDir.resolve("mf2.mp3").toFile(); + FileUtils.touch(mf2); + File mf3 = tempDir.resolve("mf3.mp3").toFile(); + FileUtils.touch(mf3); + builder.append("File1=").append(mf1.getAbsolutePath()).append("\n"); + builder.append("File2=").append(mf2.getAbsolutePath()).append("\n"); + builder.append("File3=").append(mf3.getAbsolutePath()).append("\n"); + doAnswer(new PersistPlayList(23)).when(playlistService).createPlaylist(playlistCaptor.capture()); + when(playlistService.setFilesInPlaylist(eq(23), any())).thenAnswer((Answer) invocationOnMock -> { + Playlist playlist = playlistCaptor.getValue(); + playlist.setId(invocationOnMock.getArgument(0)); + List mediaFiles = invocationOnMock.getArgument(1); + playlist.setMediaFiles(mediaFiles); + playlist.setFileCount(mediaFiles.size()); + playlist.setDuration(mediaFiles.stream().mapToDouble(MediaFile::getDuration).sum()); + return playlist; + }); + doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); + InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); + String path = "/path/to/" + playlistName + ".pls"; + User user = new User(); + user.setUsername(username); + + // when + Playlist actual = playlistFileService.importPlaylist(user, playlistName, path, Paths.get(path), inputStream, null); + + // then + verify(playlistService).createPlaylist(any(Playlist.class)); + verify(playlistService).setFilesInPlaylist(anyInt(), any()); + verify(playlistService).broadcast(any()); + verify(playlistService).broadcastFileChange(eq(23), eq(true), eq(true)); + verifyNoMoreInteractions(playlistService); + Playlist expected = new Playlist(); + expected.setUsername(username); + expected.setName(playlistName); + expected.setComment("Auto-imported from " + path); + expected.setImportedFrom(path); + expected.setShared(true); + expected.setId(23); + expected.setFileCount(3); + expected.setDuration(369.0); + assertTrue(EqualsBuilder.reflectionEquals(actual, expected, "created", "changed", "sharedUsers", "mediaFiles")); + assertEquals(3, actual.getMediaFiles().size()); + } + + @Test + public void testImportFromXSPF() throws Exception { + String username = "testUser"; + String playlistName = "test-playlist"; + StringBuilder builder = new StringBuilder(); + builder.append("\n" + + "\n" + + " \n"); + + File mf1 = tempDir.resolve("mf1.mp3").toFile(); + FileUtils.touch(mf1); + File mf2 = tempDir.resolve("mf2.mp3").toFile(); + FileUtils.touch(mf2); + File mf3 = tempDir.resolve("mf3.mp3").toFile(); + FileUtils.touch(mf3); + builder.append("file://").append(mf1.getAbsolutePath()).append("\n"); + builder.append("file://").append(mf2.getAbsolutePath()).append("\n"); + builder.append("file://").append(mf3.getAbsolutePath()).append("\n"); + builder.append(" \n" + "\n"); + doAnswer(new PersistPlayList(23)).when(playlistService).createPlaylist(playlistCaptor.capture()); + when(playlistService.setFilesInPlaylist(eq(23), any())).thenAnswer((Answer) invocationOnMock -> { + Playlist playlist = playlistCaptor.getValue(); + playlist.setId(invocationOnMock.getArgument(0)); + List mediaFiles = invocationOnMock.getArgument(1); + playlist.setMediaFiles(mediaFiles); + playlist.setFileCount(mediaFiles.size()); + playlist.setDuration(mediaFiles.stream().mapToDouble(MediaFile::getDuration).sum()); + return playlist; + }); + doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); + InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); + String path = "/path/to/" + playlistName + ".xspf"; + User user = new User(); + user.setUsername(username); + // when + Playlist actual = playlistFileService.importPlaylist(user, playlistName, path, Paths.get(path), inputStream, null); + + // then + verify(playlistService).createPlaylist(any(Playlist.class)); + verify(playlistService).setFilesInPlaylist(anyInt(), any()); + verify(playlistService).broadcast(any()); + verify(playlistService).broadcastFileChange(eq(23), eq(true), eq(true)); + verifyNoMoreInteractions(playlistService); + Playlist expected = new Playlist(); + expected.setUsername(username); + expected.setName(playlistName); + expected.setComment("Auto-imported from " + path); + expected.setImportedFrom(path); + expected.setShared(true); + expected.setId(23); + expected.setFileCount(3); + expected.setDuration(369.0); + assertTrue(EqualsBuilder.reflectionEquals(actual, expected, "created", "changed", "sharedUsers", "mediaFiles")); + assertEquals(3, actual.getMediaFiles().size()); + } + + private class PersistPlayList implements Answer { + private final int id; + + public PersistPlayList(int id) { + this.id = id; + } + + @Override + public Object answer(InvocationOnMock invocationOnMock) { + Playlist playlist = invocationOnMock.getArgument(0); + playlist.setId(id); + return playlist; + } + } + + + private class MediaFileHasEverything implements Answer { + + @Override + public MediaFile answer(InvocationOnMock invocationOnMock) { + Path path = invocationOnMock.getArgument(0); + MediaFile mediaFile = new MediaFile(); + mediaFile.setPath(path.toString()); + mediaFile.setDuration(123.0); + return mediaFile; + } + } +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestImport.java b/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestImport.java deleted file mode 100644 index a636fef57..000000000 --- a/airsonic-main/src/test/java/org/airsonic/player/service/PlaylistServiceTestImport.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.airsonic.player.service; - -import com.google.common.collect.Lists; -import org.airsonic.player.dao.MediaFileDao; -import org.airsonic.player.dao.PlaylistDao; -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.Playlist; -import org.airsonic.player.service.playlist.DefaultPlaylistImportHandler; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; - -@RunWith(MockitoJUnitRunner.class) -public class PlaylistServiceTestImport { - - PlaylistService playlistService; - - @InjectMocks - DefaultPlaylistImportHandler defaultPlaylistImportHandler; - - @Mock - MediaFileDao mediaFileDao; - - @Mock - PlaylistDao playlistDao; - - @Mock - MediaFileService mediaFileService; - - @Mock - SettingsService settingsService; - - @Mock - private UserService userService; - - @Mock - SecurityService securityService; - - @Mock - private SimpMessagingTemplate brokerTemplate; - - @Mock - private PathWatcherService pathWatcherService; - - @Rule - public TemporaryFolder folder = new TemporaryFolder(); - - @Captor - ArgumentCaptor actual; - - @Captor - ArgumentCaptor> medias; - - @Before - public void setup() { - playlistService = new PlaylistService( - mediaFileDao, - playlistDao, - userService, - securityService, - settingsService, - Collections.emptyList(), - Lists.newArrayList(defaultPlaylistImportHandler), - brokerTemplate, - pathWatcherService); - - } - - @Test - public void testImportFromM3U() throws Exception { - String username = "testUser"; - String playlistName = "test-playlist"; - StringBuilder builder = new StringBuilder(); - builder.append("#EXTM3U\n"); - File mf1 = folder.newFile(); - FileUtils.touch(mf1); - File mf2 = folder.newFile(); - FileUtils.touch(mf2); - File mf3 = folder.newFile(); - FileUtils.touch(mf3); - builder.append(mf1.getAbsolutePath()).append("\n"); - builder.append(mf2.getAbsolutePath()).append("\n"); - builder.append(mf3.getAbsolutePath()).append("\n"); - doAnswer(new PersistPlayList(23)).when(playlistDao).createPlaylist(any()); - doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); - InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); - String path = "/path/to/" + playlistName + ".m3u"; - playlistService.importPlaylist(username, playlistName, path, Paths.get(path), inputStream, null); - verify(playlistDao).createPlaylist(actual.capture()); - verify(playlistDao).setFilesInPlaylist(eq(23), medias.capture()); - Playlist expected = new Playlist(); - expected.setUsername(username); - expected.setName(playlistName); - expected.setComment("Auto-imported from " + path); - expected.setImportedFrom(path); - expected.setShared(true); - expected.setId(23); - assertTrue("\n" + ToStringBuilder.reflectionToString(actual.getValue()) + "\n\n did not equal \n\n" + ToStringBuilder.reflectionToString(expected), EqualsBuilder.reflectionEquals(actual.getValue(), expected, "created", "changed")); - List mediaFiles = medias.getValue(); - assertEquals(3, mediaFiles.size()); - } - - @Test - public void testImportFromPLS() throws Exception { - String username = "testUser"; - String playlistName = "test-playlist"; - StringBuilder builder = new StringBuilder(); - builder.append("[playlist]\n"); - File mf1 = folder.newFile(); - FileUtils.touch(mf1); - File mf2 = folder.newFile(); - FileUtils.touch(mf2); - File mf3 = folder.newFile(); - FileUtils.touch(mf3); - builder.append("File1=").append(mf1.getAbsolutePath()).append("\n"); - builder.append("File2=").append(mf2.getAbsolutePath()).append("\n"); - builder.append("File3=").append(mf3.getAbsolutePath()).append("\n"); - doAnswer(new PersistPlayList(23)).when(playlistDao).createPlaylist(any()); - doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); - InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); - String path = "/path/to/" + playlistName + ".pls"; - playlistService.importPlaylist(username, playlistName, path, Paths.get(path), inputStream, null); - verify(playlistDao).createPlaylist(actual.capture()); - verify(playlistDao).setFilesInPlaylist(eq(23), medias.capture()); - Playlist expected = new Playlist(); - expected.setUsername(username); - expected.setName(playlistName); - expected.setComment("Auto-imported from " + path); - expected.setImportedFrom(path); - expected.setShared(true); - expected.setId(23); - assertTrue("\n" + ToStringBuilder.reflectionToString(actual.getValue()) + "\n\n did not equal \n\n" + ToStringBuilder.reflectionToString(expected), EqualsBuilder.reflectionEquals(actual.getValue(), expected, "created", "changed")); - List mediaFiles = medias.getValue(); - assertEquals(3, mediaFiles.size()); - } - - @Test - public void testImportFromXSPF() throws Exception { - String username = "testUser"; - String playlistName = "test-playlist"; - StringBuilder builder = new StringBuilder(); - builder.append("\n" - + "\n" - + " \n"); - File mf1 = folder.newFile(); - FileUtils.touch(mf1); - File mf2 = folder.newFile(); - FileUtils.touch(mf2); - File mf3 = folder.newFile(); - FileUtils.touch(mf3); - builder.append("file://").append(mf1.getAbsolutePath()).append("\n"); - builder.append("file://").append(mf2.getAbsolutePath()).append("\n"); - builder.append("file://").append(mf3.getAbsolutePath()).append("\n"); - builder.append(" \n" + "\n"); - doAnswer(new PersistPlayList(23)).when(playlistDao).createPlaylist(any()); - doAnswer(new MediaFileHasEverything()).when(mediaFileService).getMediaFile(any(Path.class)); - InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); - String path = "/path/to/" + playlistName + ".xspf"; - playlistService.importPlaylist(username, playlistName, path, Paths.get(path), inputStream, null); - verify(playlistDao).createPlaylist(actual.capture()); - verify(playlistDao).setFilesInPlaylist(eq(23), medias.capture()); - Playlist expected = new Playlist(); - expected.setUsername(username); - expected.setName(playlistName); - expected.setComment("Auto-imported from " + path); - expected.setImportedFrom(path); - expected.setShared(true); - expected.setId(23); - assertTrue("\n" + ToStringBuilder.reflectionToString(actual.getValue()) + "\n\n did not equal \n\n" + ToStringBuilder.reflectionToString(expected), EqualsBuilder.reflectionEquals(actual.getValue(), expected, "created", "changed")); - List mediaFiles = medias.getValue(); - assertEquals(3, mediaFiles.size()); - } - - private class PersistPlayList implements Answer { - private final int id; - public PersistPlayList(int id) { - this.id = id; - } - - @Override - public Object answer(InvocationOnMock invocationOnMock) { - Playlist playlist = invocationOnMock.getArgument(0); - playlist.setId(id); - return null; - } - } - - private class MediaFileHasEverything implements Answer { - - @Override - public MediaFile answer(InvocationOnMock invocationOnMock) { - File file = invocationOnMock.getArgument(0); - MediaFile mediaFile = new MediaFile(); - mediaFile.setPath(file.getPath()); - return mediaFile; - } - } -}