Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/trivy_scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v5

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.32.0
uses: aquasecurity/trivy-action@0.33.0
with:
scan-type: 'fs'
format: 'sarif'
Expand Down
2 changes: 1 addition & 1 deletion airsonic-main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jmx</artifactId>
<version>4.2.33</version>
<version>4.2.35</version>
</dependency>
<!-- END Metrics -->

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package org.airsonic.player.ajax;

import org.airsonic.player.domain.MediaFile;
import org.airsonic.player.domain.MusicFolder;
import org.airsonic.player.domain.Player;
import org.airsonic.player.domain.Playlist;
import org.airsonic.player.i18n.LocaleResolver;
import org.airsonic.player.service.MediaFileService;
import org.airsonic.player.service.MediaFolderService;
import org.airsonic.player.service.PlayerService;
import org.airsonic.player.service.PlaylistService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
Expand All @@ -26,7 +22,6 @@
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -38,10 +33,6 @@ public class PlaylistWSController {
@Autowired
private PlaylistService playlistService;
@Autowired
private MediaFolderService mediaFolderService;
@Autowired
private PlayerService playerService;
@Autowired
private LocaleResolver localeResolver;

@SubscribeMapping("/readable")
Expand All @@ -55,66 +46,34 @@ public List<Playlist> 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 result = createPlaylist(dateFormat.format(now.atZone(ZoneId.systemDefault())), false, p.getName());
Playlist result = playlistService.createPlaylist(dateFormat.format(now.atZone(ZoneId.systemDefault())), false, p.getName());
playlistService.broadcast(result);
return result.getId();
}

@MessageMapping("/create/starred")
@SendToUser(broadcast = false)
public int createPlaylistForStarredSongs(Principal p) {
Locale locale = localeResolver.resolveLocale(p.getName());
DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale);

ResourceBundle bundle = ResourceBundle.getBundle("org.airsonic.player.i18n.ResourceBundle", locale);
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<MusicFolder> musicFolders = mediaFolderService.getMusicFoldersForUser(username);
List<MediaFile> songs = mediaFileService.getStarredSongs(0, Integer.MAX_VALUE, username, musicFolders);
Integer playlistId = result.getId();
playlistService.setFilesInPlaylist(playlistId, songs);
Locale locale = localeResolver.resolveLocale(username);
Integer playlistId = playlistService.createPlaylistForStarredSongs(username, locale);
playlistService.broadcastFileChange(playlistId, false, true);
return result.getId();
return playlistId;
}

@MessageMapping("/create/playqueue")
@SendToUser(broadcast = false)
public int createPlaylistForPlayQueue(Principal p, Integer playerId) throws Exception {
Player player = playerService.getPlayerById(playerId);
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.setShared(false);
playlist.setName(dateFormat.format(now.atZone(ZoneId.systemDefault())));

Playlist result = createPlaylist(dateFormat.format(now.atZone(ZoneId.systemDefault())), false, p.getName());
Integer playlistId = result.getId();
playlistService.setFilesInPlaylist(playlistId, player.getPlayQueue().getFiles());
String username = p.getName();
Locale locale = localeResolver.resolveLocale(username);
Integer playlistId = playlistService.createPlaylistForPlayQueue(playerId, username, locale);
playlistService.broadcastFileChange(playlistId, false, true);

return playlistId;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
/*
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 <http://www.gnu.org/licenses/>.

Copyright 2025 (C) Y.Tory
*/
package org.airsonic.player.repository;

import org.airsonic.player.domain.MediaFile;
import org.airsonic.player.domain.PlaylistMediaFile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PlaylistMediaFileRepository extends JpaRepository<PlaylistMediaFile, Integer> {

@Query("""
select pmf.mediaFile
from PlaylistMediaFile pmf
where pmf.playlist.id = :playlistId
order by pmf.orderIndex
""")
List<MediaFile> findMediaFilesByPlaylistId(@Param("playlistId") Integer playlistId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@
package org.airsonic.player.service;

import org.airsonic.player.domain.MediaFile;
import org.airsonic.player.domain.MusicFolder;
import org.airsonic.player.domain.PlayQueue;
import org.airsonic.player.domain.Player;
import org.airsonic.player.domain.Playlist;
import org.airsonic.player.domain.PlaylistMediaFile;
import org.airsonic.player.domain.User;
import org.airsonic.player.repository.PlaylistMediaFileRepository;
import org.airsonic.player.repository.PlaylistRepository;
import org.airsonic.player.repository.UserRepository;
import org.airsonic.player.service.cache.PlaylistCache;
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.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -40,6 +42,9 @@
import jakarta.annotation.Nonnull;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -55,14 +60,34 @@ public class PlaylistService {

private static final Logger LOG = LoggerFactory.getLogger(PlaylistService.class);

@Autowired
private UserRepository userRepository;
@Autowired
private PlaylistRepository playlistRepository;
@Autowired
private AsyncWebSocketClient asyncWebSocketClient;
@Autowired
private PlaylistCache playlistCache;
private final UserRepository userRepository;
private final PlaylistRepository playlistRepository;
private final AsyncWebSocketClient asyncWebSocketClient;
private final PlaylistCache playlistCache;
private final PlaylistMediaFileRepository playlistMediaFileRepository;
private final MediaFileService mediaFileService;
private final MediaFolderService mediaFolderService;
private final PlayerService playerService;

public PlaylistService(
UserRepository userRepository,
PlaylistRepository playlistRepository,
AsyncWebSocketClient asyncWebSocketClient,
PlaylistCache playlistCache,
PlaylistMediaFileRepository playlistMediaFileRepository,
PlayerService playerService,
MediaFolderService mediaFolderService,
MediaFileService mediaFileService
) {
this.userRepository = userRepository;
this.playlistRepository = playlistRepository;
this.asyncWebSocketClient = asyncWebSocketClient;
this.playlistCache = playlistCache;
this.playlistMediaFileRepository = playlistMediaFileRepository;
this.playerService = playerService;
this.mediaFolderService = mediaFolderService;
this.mediaFileService = mediaFileService;
}


/**
Expand Down Expand Up @@ -142,25 +167,18 @@ public List<String> getPlaylistUsers(Integer playlistId) {
return result;
}

@Transactional(readOnly = true)
public List<MediaFile> getFilesInPlaylist(int id) {
return getFilesInPlaylist(id, false);
}


@Transactional(readOnly = true)
public List<MediaFile> getFilesInPlaylist(int id, boolean includeNotPresent) {
return playlistRepository.findById(id).map(p -> {
return p.getPlaylistMediaFiles().stream()
.map(PlaylistMediaFile::getMediaFile)
return playlistMediaFileRepository.findMediaFilesByPlaylistId(id).stream()
.filter(Objects::nonNull)
.filter(x -> x.isPresent() || includeNotPresent)
.collect(Collectors.toList());
}).orElseGet(
() -> {
LOG.warn("Playlist {} not found", id);
return new ArrayList<>();
}
);
}

private List<MediaFile> filterNoDurationFiles(List<MediaFile> files) {
Expand Down Expand Up @@ -189,7 +207,6 @@ public Playlist setFilesInPlaylist(int id, List<MediaFile> files) {
});
}


private Playlist setFilesInPlaylist(Playlist playlist, List<MediaFile> files) {

List<MediaFile> filteredFiles = filterNoDurationFiles(files);
Expand Down Expand Up @@ -431,6 +448,48 @@ public void broadcastFileChange(Integer id, boolean isShared, boolean filesChang
});
}

/**
* Creates a new playlist for the specified play queue.
*
* @param playerId the ID of the player
* @param username the username of the user creating the playlist
* @param locale the locale to use for formatting
* @return the ID of the created playlist
*/
@Transactional
public Integer createPlaylistForPlayQueue(@Nonnull Integer playerId, @Nonnull String username, Locale locale) {
DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale);
Instant now = Instant.now();
String name = dateFormat.format(now.atZone(ZoneId.systemDefault()));

Playlist playlist = createPlaylist(name, false, username);
broadcast(playlist);

Player player = playerService.getPlayerById(playerId);
playlist = setFilesInPlaylist(playlist, player.getPlayQueue().getFiles());

playlistRepository.saveAndFlush(playlist);
return playlist.getId();
}

@Transactional
public Integer createPlaylistForStarredSongs(@Nonnull String username, Locale locale) {

DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale);
ResourceBundle bundle = ResourceBundle.getBundle("org.airsonic.player.i18n.ResourceBundle", locale);
Instant now = Instant.now();
String name = bundle.getString("top.starred") + " " + dateFormat.format(now.atZone(ZoneId.systemDefault()));

Playlist playlist = createPlaylist(name, false, username);
broadcast(playlist);

List<MusicFolder> musicFolders = mediaFolderService.getMusicFoldersForUser(username);
List<MediaFile> musicFiles = mediaFileService.getStarredSongs(0, Integer.MAX_VALUE, username, musicFolders);
setFilesInPlaylist(playlist, musicFiles);
Integer playlistId = playlist.getId();
return playlistId;
}

public static class BroadcastedPlaylist extends Playlist {
private final boolean filesChanged;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.airsonic.player.service;

import org.springframework.stereotype.Service;

@Service
public class StreamService {

}
Loading