diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 5d0ac36de..4957207b3 100644 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -592,7 +592,7 @@ com.mysql mysql-connector-j - 8.4.0 + 9.0.0 runtime diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/EditMediaDirController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/EditMediaDirController.java new file mode 100644 index 000000000..1c9c228bd --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/EditMediaDirController.java @@ -0,0 +1,61 @@ +/* + 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 2024 (C) Y.Tory + */ +package org.airsonic.player.controller; + +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.service.MediaFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * Controller for updating Media file + * + * @author Y. Tory + */ +@Controller +@RequestMapping({"/editMediaDir"}) +public class EditMediaDirController { + + @Autowired + private MediaFileService mediaFileService; + + @PostMapping + protected ModelAndView handleRequestInternal(HttpServletRequest request) throws Exception { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + String action = request.getParameter("action"); + + MediaFile mediaFile = mediaFileService.getMediaFile(id); + + if ("editMediaDirTitle".equals(action) && mediaFile != null && mediaFile.isDirectory()) { + mediaFile.setTitle(request.getParameter("mediaDirTitle")); + mediaFileService.updateMediaFile(mediaFile); + } + + String url = "main.view?id=" + id; + return new ModelAndView(new RedirectView(url)); + } + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/SearchController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/SearchController.java index 90a3cf243..38f87fd77 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/SearchController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/SearchController.java @@ -132,7 +132,8 @@ private List createArtistResults(SearchResult artists) { Map, SearchResultArtist> artistMap = new LinkedHashMap<>(); artists.getMediaFiles().stream().forEach(m -> { - String artist = Optional.ofNullable(m.getArtist()) + String artist = Optional.ofNullable(m.getTitle()) + .or(() -> Optional.ofNullable(m.getArtist())) .or(() -> Optional.ofNullable(m.getAlbumArtist())) .orElse("(Unknown)"); SearchResultArtist artistResult = artistMap.computeIfAbsent(Pair.of(artist, m.getFolder().getId()), 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 8096cd634..fad027c82 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 @@ -519,7 +519,7 @@ private T createJaxbArtist(T jaxbArtist, org.airsonic.play private org.subsonic.restapi.Artist createJaxbArtist(MediaFile artist, String username) { org.subsonic.restapi.Artist result = new org.subsonic.restapi.Artist(); result.setId(String.valueOf(artist.getId())); - result.setName(artist.getArtist()); + result.setName(artist.getTitle() != null ? artist.getTitle() : artist.getArtist()); Instant starred = mediaFileService.getMediaFileStarredDate(artist, username); result.setStarred(jaxbWriter.convertDate(starred)); return result; diff --git a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java index b75b0d85d..92d0965b9 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java +++ b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java @@ -279,7 +279,7 @@ public SecurityFilterChain webSecurityFilterChain(HttpSecurity http, Authenticat "/databaseSettings*", "/transcodeSettings*", "/rest/startScan*").hasRole("ADMIN") .requestMatchers("/deletePlaylist*", "/savePlaylist*").hasRole("PLAYLIST").requestMatchers("/download*").hasRole("DOWNLOAD") .requestMatchers("/upload*").hasRole("UPLOAD").requestMatchers("/createShare*").hasRole("SHARE") - .requestMatchers("/changeCoverArt*", "/editTags*").hasRole("COVERART").requestMatchers("/setMusicFileInfo*").hasRole("COMMENT") + .requestMatchers("/changeCoverArt*", "/editTags*", "/editMediaDir*").hasRole("COVERART").requestMatchers("/setMusicFileInfo*").hasRole("COMMENT") .requestMatchers("/podcastReceiverAdmin*", "/podcastEpisodes*").hasRole("PODCAST") .requestMatchers("/**").hasRole("USER").anyRequest().authenticated()) .formLogin((login) -> login diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtCreateService.java b/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtCreateService.java index 5875bc72b..11d6f79e1 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtCreateService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtCreateService.java @@ -50,6 +50,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import javax.imageio.IIOException; import javax.imageio.ImageIO; import java.awt.Graphics2D; @@ -231,9 +232,13 @@ public BufferedImage createImage(CoverArtRequest coverArtRequest, int size) { return ImageUtil.scaleToSquare(bimg, size); } } - LOG.warn("Failed to process cover art {}: {} failed", coverArt, reason); + LOG.warn("Failed to process cover art {}: {} failed", coverArt.getFullPath(), reason); + } catch (IIOException x) { + LOG.warn("Failed to process cover art {}: {}", coverArt.getFullPath(), "Bad image file"); + LOG.debug(x.getMessage(), x); } catch (Throwable x) { - LOG.warn("Failed to process cover art {}", coverArt, x); + LOG.warn("Failed to process cover art {}", coverArt.getFullPath()); + LOG.debug(x.getMessage(), x); } } return createAutoCover(coverArtRequest, size, size); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/search/DocumentFactory.java b/airsonic-main/src/main/java/org/airsonic/player/service/search/DocumentFactory.java index fe8d9e02b..afdc0ff83 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/search/DocumentFactory.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/search/DocumentFactory.java @@ -165,7 +165,7 @@ public Document createAlbumDocument(MediaFile mediaFile, MusicFolder musicFolder public Document createArtistDocument(MediaFile mediaFile, MusicFolder musicFolder) { Document doc = new Document(); fieldId.accept(doc, mediaFile.getId()); - fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist()); + fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getName()); fieldFolderPath.accept(doc, musicFolder.getPath().toString()); return doc; } diff --git a/airsonic-main/src/main/resources/templates/mediaMain.html b/airsonic-main/src/main/resources/templates/mediaMain.html index 1b1063257..bc1b78aaa 100644 --- a/airsonic-main/src/main/resources/templates/mediaMain.html +++ b/airsonic-main/src/main/resources/templates/mediaMain.html @@ -45,9 +45,14 @@ #mediaDirTitle { font-size: 150%; padding: 0; - border: 0; + border: 1px solid transparent; margin: 0; } + #mediaDirTitleForm input[type="text"] { + font-size: 150%; + padding: 0; + margin: 0; + } #mediaDirController span { margin-right: 0.3em; } @@ -122,6 +127,12 @@ // comments updateComments(); + // directory name + // only for artist directories + if (mediaDir.contentType == 'artist') { + updateMediaDirTitle(); + } + // search components if (mediaDir.album == null && mediaDir.artist == null) { $('.external-search').hide(); @@ -274,6 +285,11 @@ $('#commentForm input[name="id"]').val(mediaDir.id); } + function updateMediaDirTitle() { + $('#mediaDirTitleForm input[name="mediaDirTitle"]').val(mediaDir.title); + $('#mediaDirTitleForm input[name="id"]').val(mediaDir.id); + } + function subDirsHeading() { var subDirHeading = 'Subdirectories'; @@ -839,6 +855,8 @@ if (artistInfo.artistBio && artistInfo.artistBio.biography) { $("#artistBio").html(artistInfo.artistBio.biography); + } else { + $("#artistBio").empty(); } this.topSongs = artistInfo.topSongs; @@ -861,6 +879,9 @@ var ancestor = mediaDir.ancestors[i]; ancestors.append(feather.icons.folder.toSvg({class: 'feather-sm'})); ancestors.append($("").attr("href", "#").attr("onclick", "getMediaDirectory(" + ancestor.id + ")").text(ancestor.title)); + if (ancestor.title != ancestor.artist && ancestor.entryType == 'DIRECTORY' && ancestor.artist != null) { + ancestors.append(" (").append(ancestor.artist).append(")"); + } ancestors.append(" » "); } } @@ -873,6 +894,9 @@ $("#mediaDirTitle").append(feather.icons.folder.toSvg()); } $("#mediaDirTitle").append(mediaDir.title); + if (mediaDir.title != mediaDir.artist && mediaDir.contentType == 'artist') { + $("#mediaDirTitle").append(" (").append(mediaDir.artist).append(")"); + } } function toggleMediaDirStar(status) { @@ -956,6 +980,11 @@ $("#commentForm").toggle(); $("#comment").toggle(); } + + function toggleMediaDirTitle() { + $("#mediaDirTitleForm").toggle(); + $("#mediaDirTitle").toggle(); + } /** Albums Only **/ // actionSelected() is invoked when the users selects from the "More actions..." combo box. @@ -1149,7 +1178,14 @@

-
+ +
+
+ +
+
@@ -1205,6 +1245,7 @@
+
diff --git a/airsonic-main/src/main/resources/templates/podcastChannel.html b/airsonic-main/src/main/resources/templates/podcastChannel.html index 065e18a0d..fa2c7f9ce 100644 --- a/airsonic-main/src/main/resources/templates/podcastChannel.html +++ b/airsonic-main/src/main/resources/templates/podcastChannel.html @@ -186,11 +186,11 @@

- - - + + + - + diff --git a/docs/README.md b/docs/README.md index d6946c56e..e77c50a10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,18 +3,16 @@ ## Contents - [First start](./first_start/README.md) - - [WebUI](./webui/README.md) - - [Podcast](./webui/podcast.md) - + - [Media](./webui/media.md) + - [Podcast](./webui/podcast.md) - [Configures](./configures/README.md) - [Detail Configuration](./configures/detail.md) - Proxy - [Prerequisites](./proxy/README.md) - [Apache](./proxy/apache.md) - - Media + - [Rule](./media/rule.md) - [Cover Art/ Artist Image](./media/coverart.md) - [Jukebox](./media/jukebox.md) - - [TroubleShooting](./troubleshooting.md) diff --git a/docs/figures/webui-media-artist-edit.png b/docs/figures/webui-media-artist-edit.png new file mode 100644 index 000000000..a09ea08fc Binary files /dev/null and b/docs/figures/webui-media-artist-edit.png differ diff --git a/docs/figures/webui-media-artist.png b/docs/figures/webui-media-artist.png new file mode 100644 index 000000000..35859fa3d Binary files /dev/null and b/docs/figures/webui-media-artist.png differ diff --git a/docs/media/rule.md b/docs/media/rule.md new file mode 100644 index 000000000..67e7a9d4f --- /dev/null +++ b/docs/media/rule.md @@ -0,0 +1,35 @@ +# Rule + +Airsonic Advanced categorizes directories and files into Album, Artist, Song, and Video using the following logic: + +| Type | Description | +| --- | --- | +| Song | A single audio file. If it has the specified extensions which defined in the `Settings` > `General` > `Music files` field, it is considered a song. | +| Video | A single video file. If it has the specified extensions which defined in the `Settings` > `General` > `Video files` field, it is considered a video. | +| Album | A parent directory containing at least one Song or Video. | +| Artist | A parent directory containing albums. If it contains songs or videos directly, it is considered an album. | + + +Therefore, it is understood as follows: + +``` +. +├── Artist1 +│ ├── Album1 +│ │ ├── Song1.flac +│ │ ├── Song2.mp3 +│ │ └── Folder.jpg +│ ├── Artist2 +│ │ ├── Album2 +│ │ │ ├── Song3.ogg +│ │ │ ├── Song4.ogg +│ │ │ └── Folder.jpg +│ │ └──Album3 +│ │ ├── Song5.mp3 +│ │ ├── Song6.mp3 +│ │ └── Folder.jpeg +├── Album2 +│ ├── Song7.mp3 +│ ├── Video1.mp4 +│ └── Folder.jpg +``` \ No newline at end of file diff --git a/docs/webui/README.md b/docs/webui/README.md index cc11313da..05ccd0899 100644 --- a/docs/webui/README.md +++ b/docs/webui/README.md @@ -4,4 +4,5 @@ This document describes the features of the Airsonic Advanced web UI. ## Contents +- [Media](./media.md) - [Podcast](./podcast.md) \ No newline at end of file diff --git a/docs/webui/media.md b/docs/webui/media.md new file mode 100644 index 000000000..849eaf32f --- /dev/null +++ b/docs/webui/media.md @@ -0,0 +1,39 @@ +# Media + +The media section of the web UI allows you to browse and play your music. + +## Artist + +### Artist View + +![webui-media-artist](../figures/webui-media-artist.png) + +| Number | Description | Role | +| --- | --- | --- | +| 1 | Parent Directory Links. You can click on the name to navigate to the parent directories. | User | +| 2 | Artist name. If you edit the artist name, the original name will be displayed in `( )`. | User | +| 3 | Star. You can click on the star to add the artist to your favorites. | User | +| 4 | Play. You can click on the play button to play all songs by the artist. | User | +| 5 | Shuffle. You can click on the shuffle button to shuffle all songs by the artist. | User | +| 6 | Add to player. You can click on the add to player button to add all songs by the artist to a player. | User | +| 7 | Edit artist. You can click on the edit artist button to edit the artist name. | CoverArt | +| 8 | Comment. You can click on the comment button to add a comment to the artist. | Comment | +| 9 | List view. You can click on the list view button to switch to the list view. | User | +| 10 | Grid view. You can click on the grid view button to switch to the grid view. | User | +| 11 | Albums. You can see the albums by the artist. | User | +| 12 | Artist Information. You can see the artist information from Last.fm. | User | + +### Edit Artist + +![webui-media-artist-edit](../figures/webui-media-artist-edit.png) + +1. Click on the edit artist button. +2. Edit the artist name. +3. Click on the save button. + +If you want to cancel, click on the Edit Artist button again. +To update the search results, performing a library rescan is necessary, but a full scan is unnecessary. + +## Related Documentation + +- [Media/Rule](../media/rule.md) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1ac39056d..56c5afd16 100644 --- a/pom.xml +++ b/pom.xml @@ -151,7 +151,7 @@ org.checkerframework checker-qual - 3.44.0 + 3.45.0 com.google.j2objc @@ -280,7 +280,7 @@ org.owasp dependency-check-maven - 9.2.0 + 10.0.1 true 24