diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 8702aa64f..340e37914 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.20.0 + uses: aquasecurity/trivy-action@0.21.0 with: scan-type: 'fs' format: 'sarif' diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 6fbc46fdd..d79e4b500 100644 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -209,7 +209,7 @@ org.apache.commons commons-compress - 1.26.1 + 1.26.2 @@ -282,7 +282,7 @@ org.eclipse.persistence org.eclipse.persistence.moxy - 4.0.2 + 4.0.3 @@ -591,7 +591,7 @@ com.mysql mysql-connector-j - 8.3.0 + 8.4.0 runtime diff --git a/airsonic-main/src/main/java/org/airsonic/player/command/PodcastChannelCommand.java b/airsonic-main/src/main/java/org/airsonic/player/command/PodcastChannelCommand.java new file mode 100644 index 000000000..e02cdde41 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/command/PodcastChannelCommand.java @@ -0,0 +1,73 @@ +/* + * 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 + * Copyright 2015 (C) Sindre Mehus + */ +package org.airsonic.player.command; + +import org.airsonic.player.domain.PodcastChannel; +import org.airsonic.player.domain.PodcastEpisode; +import org.airsonic.player.domain.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class PodcastChannelCommand { + + private User user; + private PodcastChannel channel; + private List episodes = new ArrayList<>(); + private boolean partyModeEnabled; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public PodcastChannel getChannel() { + return channel; + } + + public void setChannel(PodcastChannel channel) { + this.channel = channel; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } + + public void setEpisodesByDAO(List episodes) { + this.episodes = episodes.stream().map(PodcastEpisodeCommand::new).collect(Collectors.toList()); + } + + public boolean isPartyModeEnabled() { + return partyModeEnabled; + } + + public void setPartyModeEnabled(boolean partyModeEnabled) { + this.partyModeEnabled = partyModeEnabled; + } + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/command/PodcastEpisodeCommand.java b/airsonic-main/src/main/java/org/airsonic/player/command/PodcastEpisodeCommand.java new file mode 100644 index 000000000..42c904e21 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/command/PodcastEpisodeCommand.java @@ -0,0 +1,156 @@ +/* + * 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.command; + +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.domain.PodcastEpisode; +import org.airsonic.player.domain.PodcastStatus; + +import java.time.Instant; + +public class PodcastEpisodeCommand { + + private Integer id; + + private MediaFile mediaFile; + + private String title; + + private String description; + + private Instant publishDate; + + private String duration; + + private PodcastStatus status; + + private boolean locked; + + private Double completionRate; + + private String errorMessage; + + private boolean selected; + + public PodcastEpisodeCommand() { + } + + public PodcastEpisodeCommand(PodcastEpisode episode) { + this.id = episode.getId(); + this.mediaFile = episode.getMediaFile(); + this.title = episode.getTitle(); + this.description = episode.getDescription(); + this.publishDate = episode.getPublishDate(); + this.duration = episode.getDuration(); + this.status = episode.getStatus(); + this.locked = episode.isLocked(); + this.completionRate = episode.getCompletionRate(); + this.errorMessage = episode.getErrorMessage(); + this.selected = false; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Instant getPublishDate() { + return publishDate; + } + + public void setPublishDate(Instant publishDate) { + this.publishDate = publishDate; + } + + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } + + public Double getCompletionRate() { + return completionRate; + } + + public void setCompletionRate(Double completionRate) { + this.completionRate = completionRate; + } + + public PodcastStatus getStatus() { + return status; + } + + public void setStatus(PodcastStatus status) { + this.status = status; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public MediaFile getMediaFile() { + return mediaFile; + } + + public void setMediaFile(MediaFile mediaFile) { + this.mediaFile = mediaFile; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public boolean isSelected() { + return selected; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/PodcastChannelController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/PodcastChannelController.java index 760ad52f4..e868c5eac 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/PodcastChannelController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/PodcastChannelController.java @@ -14,25 +14,35 @@ * You should have received a copy of the GNU General Public License * along with Airsonic. If not, see . * + * Copyright 2024 (C) Y.Tory * Copyright 2015 (C) Sindre Mehus */ package org.airsonic.player.controller; +import org.airsonic.player.command.PodcastChannelCommand; +import org.airsonic.player.command.PodcastEpisodeCommand; +import org.airsonic.player.domain.PodcastStatus; +import org.airsonic.player.domain.User; +import org.airsonic.player.domain.UserSettings; +import org.airsonic.player.service.PersonalSettingsService; import org.airsonic.player.service.PodcastPersistenceService; import org.airsonic.player.service.SecurityService; +import org.airsonic.player.service.podcast.PodcastDownloadClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.util.HashMap; -import java.util.Map; +import java.util.EnumSet; +import java.util.Set; /** * Controller for the "Podcast channel" page. @@ -47,20 +57,122 @@ public class PodcastChannelController { private PodcastPersistenceService podcastService; @Autowired private SecurityService securityService; + @Autowired + private PersonalSettingsService personalSettingsService; + @Autowired + private PodcastDownloadClient podcastDownloadClient; + + private static final Set DOWNLOADABLE_STATUSES = EnumSet.of(PodcastStatus.SKIPPED, PodcastStatus.ERROR, PodcastStatus.NEW); + private static final Set LOCKABLE_STATUSES = EnumSet.of(PodcastStatus.SKIPPED, PodcastStatus.COMPLETED, PodcastStatus.NEW); + private static final Set UNLOCKABLE_STATUSES = EnumSet.of(PodcastStatus.SKIPPED, PodcastStatus.COMPLETED, PodcastStatus.NEW, PodcastStatus.ERROR, PodcastStatus.DELETED); + + @ModelAttribute + public User getUser(HttpServletRequest request) { + return securityService.getCurrentUser(request); + } @GetMapping - protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + protected ModelAndView get(@ModelAttribute User user, + @RequestParam(name = "id", required = true) Integer channelId) throws Exception { + + PodcastChannelCommand command = new PodcastChannelCommand(); + UserSettings settings = personalSettingsService.getUserSettings(user.getUsername()); + command.setUser(user); + command.setChannel(podcastService.getChannel(channelId)); + command.setEpisodesByDAO(podcastService.getEpisodes(channelId)); + command.setPartyModeEnabled(settings.getPartyModeEnabled()); - Map map = new HashMap<>(); ModelAndView result = new ModelAndView(); - result.addObject("model", map); + result.addObject("command", command); + return result; + } - int channelId = ServletRequestUtils.getRequiredIntParameter(request, "id"); + @PostMapping(params = "delete") + protected String deleteEpisodes(@ModelAttribute User user, + @ModelAttribute(name = "channelId") Integer channelId, + @ModelAttribute("command") PodcastChannelCommand command) throws Exception { + + if (!user.isPodcastRole()) { + throw new AccessDeniedException("Podcast is forbidden for user " + user.getUsername()); + } + + if (channelId == null) { + throw new IllegalArgumentException("Channel ID is null"); + } + + command.getEpisodes().stream() + .filter(ep -> ep.isSelected() && ep.getStatus() != PodcastStatus.DELETED) + .map(PodcastEpisodeCommand::getId) + .forEach(id -> podcastService.deleteEpisode(id, true)); + + return "redirect:/podcastChannel.view?id=" + channelId; + + } + + @PostMapping(params = "download") + protected String downloadEpisodes(@ModelAttribute User user, + @ModelAttribute(name = "channelId") Integer channelId, + @ModelAttribute("command") PodcastChannelCommand command) throws Exception { + + if (!user.isPodcastRole()) { + throw new AccessDeniedException("Podcast is forbidden for user " + user.getUsername()); + } + + if (channelId == null) { + throw new IllegalArgumentException("Channel ID is null"); + } + + command.getEpisodes().parallelStream() + .filter(ep -> ep.isSelected() && DOWNLOADABLE_STATUSES.contains(ep.getStatus())) + .map(PodcastEpisodeCommand::getId) + .forEach(podcastDownloadClient::downloadEpisode); + + return "redirect:/podcastChannel.view?id=" + channelId; + + } + + @PostMapping(params = "lock") + protected String lockEpisodes(@ModelAttribute User user, + @ModelAttribute(name = "channelId") Integer channelId, + @ModelAttribute("command") PodcastChannelCommand command) throws Exception { + + if (!user.isPodcastRole()) { + throw new AccessDeniedException("Podcast is forbidden for user " + user.getUsername()); + } + + if (channelId == null) { + throw new IllegalArgumentException("Channel ID is null"); + } + + command.getEpisodes().parallelStream() + .filter(ep -> ep.isSelected() && LOCKABLE_STATUSES.contains(ep.getStatus())) + .map(PodcastEpisodeCommand::getId) + .forEach(podcastService::lockEpisode); + + return "redirect:/podcastChannel.view?id=" + channelId; + + } + + @PostMapping(params = "unlock") + protected String unlockEpisodes(@ModelAttribute User user, + @ModelAttribute(name = "channelId") Integer channelId, + @ModelAttribute("command") PodcastChannelCommand command) throws Exception { + + if (!user.isPodcastRole()) { + throw new AccessDeniedException("Podcast is forbidden for user " + user.getUsername()); + } + + if (channelId == null) { + throw new IllegalArgumentException("Channel ID is null"); + } + + command.getEpisodes().parallelStream() + .filter(ep -> ep.isSelected() && UNLOCKABLE_STATUSES.contains(ep.getStatus())) + .map(PodcastEpisodeCommand::getId) + .forEach(podcastService::unlockEpisode); + + return "redirect:/podcastChannel.view?id=" + channelId; - map.put("user", securityService.getCurrentUser(request)); - map.put("channel", podcastService.getChannel(channelId)); - map.put("episodes", podcastService.getEpisodes(channelId)); - return result; } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java index 8bd507f85..1d9713367 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java @@ -74,6 +74,9 @@ public class PodcastEpisode { @Enumerated(EnumType.STRING) private PodcastStatus status; + @Column(name = "locked") + private boolean locked; + @Column(name = "error_message") private String errorMessage; @@ -212,6 +215,14 @@ public void setEpisodeGuid(String episodeGuid) { this.episodeGuid = episodeGuid; } + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + @Override public int hashCode() { return Objects.hash(url); diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java b/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java index faa42e962..1a23ef8b2 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java +++ b/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java @@ -23,6 +23,8 @@ public Optional findByChannelAndTitleAndPublishDate(PodcastChann public List findByChannel(PodcastChannel channel); + public List findByChannelAndLockedFalse(PodcastChannel channel); + public List findByChannelAndStatus(PodcastChannel channel, PodcastStatus status); public List findByStatusAndPublishDateNotNullAndMediaFilePresentTrueOrderByPublishDateDesc( diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java index eb8a0b608..4fc31c87b 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java @@ -46,6 +46,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -78,6 +81,7 @@ public class PodcastPersistenceService { private final PodcastEpisodeRepository podcastEpisodeRepository; private final PodcastRuleRepository podcastRuleRepository; private final SecurityService securityService; + private final SettingsService settingsService; private Predicate filterAllowed; @@ -88,7 +92,8 @@ public PodcastPersistenceService( AsyncWebSocketClient asyncSocketClient, PodcastChannelRepository podcastChannelRepository, PodcastEpisodeRepository podcastEpisodeRepository, - PodcastRuleRepository podcastRuleRepository + PodcastRuleRepository podcastRuleRepository, + SettingsService settingsService ) { this.mediaFileService = mediaFileService; this.mediaFolderService = mediaFolderService; @@ -96,6 +101,7 @@ public PodcastPersistenceService( this.podcastEpisodeRepository = podcastEpisodeRepository; this.podcastRuleRepository = podcastRuleRepository; this.securityService = securityService; + this.settingsService = settingsService; filterAllowed = episode -> episode.getMediaFile() == null || this.securityService.isReadAllowed(episode.getMediaFile(), false); } @@ -378,6 +384,7 @@ public List getEpisodes(Integer channelId) { // Refresh media file to check if it still exists mediaFileService.refreshMediaFile(mediaFile); if (!mediaFile.isPresent() && ep.getStatus() != PodcastStatus.DELETED) { + LOG.info("Media file '{}' for episode '{}' is not present anymore. Setting episode status to deleted.", mediaFile.getId(), ep.getTitle()); // If media file is not present anymore, set episode status to deleted ep.setStatus(PodcastStatus.DELETED); ep.setErrorMessage(null); @@ -524,6 +531,7 @@ public PodcastEpisode updateEpisode(PodcastEpisode episode) { ep.setBytesDownloaded(episode.getBytesDownloaded()); ep.setErrorMessage(episode.getErrorMessage()); ep.setStatus(episode.getStatus()); + ep.setLocked(episode.isLocked()); return podcastEpisodeRepository.save(ep); }).orElseGet(() -> { LOG.warn("Podcast episode with id {} not found", episode.getId()); @@ -577,11 +585,17 @@ public void deleteEpisode(int episodeId, boolean logicalDelete) { }); } - private void deleteEpisode(PodcastEpisode episode, boolean logicalDelete) { + private void deleteEpisode(@Nullable PodcastEpisode episode, boolean logicalDelete) { if (episode == null) { return; } + // Check if episode is locked + if (episode.getStatus() == PodcastStatus.COMPLETED && episode.isLocked()) { + LOG.info("Episode '{}' is locked and cannot be deleted", episode.getTitle()); + return; + } + // Delete file and update mediaFile MediaFile file = episode.getMediaFile(); if (file != null) { @@ -616,4 +630,85 @@ public PodcastEpisode getEpisodeByTitleAndDate(PodcastChannel channel, String ti .filter(filterAllowed) .orElse(null); } + + /** + * Deletes obsolete episodes for a given channel. + * + * @param channel The Podcast channel. + */ + @Transactional + public synchronized void deleteObsoleteEpisodes(@Nullable PodcastChannel channel) { + int episodeCount = Optional.ofNullable(channel) + .flatMap(ch -> podcastRuleRepository.findById(ch.getId())) + .map(cr -> cr.getRetentionCount()) + .orElse(settingsService.getPodcastEpisodeRetentionCount()); + if (episodeCount == -1) { + return; + } + + // Get all unlocked episodes of the channel + List episodes = getUnlockedEpisodes(channel); + + // Don't do anything if other episodes of the same channel is currently + // downloading. + if (episodes.parallelStream().anyMatch(episode -> episode.getStatus() == PodcastStatus.DOWNLOADING)) { + return; + } + + int numEpisodes = episodes.size(); + int episodesToDelete = Math.max(0, numEpisodes - episodeCount); + // Delete in reverse to get chronological order (oldest episodes first). + for (int i = 0; i < episodesToDelete; i++) { + deleteEpisode(episodes.get(numEpisodes - 1 - i), true); + LOG.info("Deleted old Podcast episode {}", episodes.get(numEpisodes - 1 - i).getUrl()); + } + } + + /** + * Returns Unlocked Podcast episodes for a given channel. + * + * @param channel The Podcast channel + * @return Possibly empty list of Unlocked Podcast episodes for the given channel, sorted in + * reverse chronological order (newest episode first). + */ + @Nonnull + private List getUnlockedEpisodes(PodcastChannel channel) { + if (Objects.isNull(channel)) return new ArrayList<>(); + return podcastEpisodeRepository.findByChannelAndLockedFalse(channel).stream() + .filter(filterAllowed) + .sorted(Comparator.comparing(PodcastEpisode::getPublishDate, Comparator.nullsLast(Comparator.reverseOrder()))) + .collect(Collectors.toList()); + } + + /** + * Lock episode + * + * @param episodeId episode id to lock + */ + @Transactional + public void lockEpisode(int episodeId) { + LOG.info("Locking episode with id {}", episodeId); + podcastEpisodeRepository.findById(episodeId).ifPresentOrElse(episode -> { + episode.setLocked(true); + podcastEpisodeRepository.save(episode); + }, () -> { + LOG.warn("Podcast episode with id {} not found", episodeId); + }); + } + + /** + * Unlock episode + * + * @param episodeId episode id to unlock + */ + @Transactional + public void unlockEpisode(int episodeId) { + LOG.info("Unlocking episode with id {}", episodeId); + podcastEpisodeRepository.findById(episodeId).ifPresentOrElse(episode -> { + episode.setLocked(false); + podcastEpisodeRepository.save(episode); + }, () -> { + LOG.warn("Podcast episode with id {} not found", episodeId); + }); + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/podcast/PodcastDownloadClient.java b/airsonic-main/src/main/java/org/airsonic/player/service/podcast/PodcastDownloadClient.java index 322f91585..3f5a6d06a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/podcast/PodcastDownloadClient.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/podcast/PodcastDownloadClient.java @@ -28,7 +28,6 @@ import org.airsonic.player.service.MediaFileService; import org.airsonic.player.service.PodcastPersistenceService; import org.airsonic.player.service.SecurityService; -import org.airsonic.player.service.SettingsService; import org.airsonic.player.service.VersionService; import org.airsonic.player.service.metadata.MetaData; import org.airsonic.player.service.metadata.MetaDataParser; @@ -59,8 +58,6 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; @Service @@ -81,9 +78,6 @@ public class PodcastDownloadClient { @Autowired private VersionService versionService; - @Autowired - private SettingsService settingsService; - @Autowired private SecurityService securityService; @@ -97,7 +91,7 @@ public CompletableFuture downloadEpisode(Integer episodeId) { } PodcastEpisode episode = podcastPersistenceService.prepareDownloadEpisode(episodeId); - if (episode != null) { + if (episode != null && episode.getUrl() != null) { LOG.info("Starting to download Podcast from {}", episode.getUrl()); PodcastChannel channel = episode.getChannel(); @@ -166,7 +160,7 @@ public CompletableFuture downloadEpisode(Integer episodeId) { updateTags(file, episode); episode.setStatus(PodcastStatus.COMPLETED); podcastPersistenceService.updateEpisode(episode); - deleteObsoleteEpisodes(channel); + podcastPersistenceService.deleteObsoleteEpisodes(channel); } } catch (Exception x) { LOG.warn("Failed to download Podcast from {}", episode.getUrl(), x); @@ -198,31 +192,6 @@ private void updateTags(MediaFile file, PodcastEpisode episode) { } } - private synchronized void deleteObsoleteEpisodes(PodcastChannel channel) { - int episodeCount = Optional.ofNullable(channel) - .map(ch -> podcastPersistenceService.getChannelRule(ch.getId())) - .map(cr -> cr.getRetentionCount()) - .orElse(settingsService.getPodcastEpisodeRetentionCount()); - if (episodeCount == -1) { - return; - } - - List episodes = podcastPersistenceService.getEpisodes(channel.getId()); - - // Don't do anything if other episodes of the same channel is currently - // downloading. - if (episodes.parallelStream().anyMatch(episode -> episode.getStatus() == PodcastStatus.DOWNLOADING)) { - return; - } - - int numEpisodes = episodes.size(); - int episodesToDelete = Math.max(0, numEpisodes - episodeCount); - // Delete in reverse to get chronological order (oldest episodes first). - for (int i = 0; i < episodesToDelete; i++) { - podcastPersistenceService.deleteEpisode(episodes.get(numEpisodes - 1 - i).getId(), true); - LOG.info("Deleted old Podcast episode {}", episodes.get(numEpisodes - 1 - i).getUrl()); - } - } private synchronized Pair createEpisodeFile(PodcastChannel channel, PodcastEpisode episode) { String filename = StringUtil.getUrlFile(PodcastUtil.sanitizeUrl(episode.getUrl(), true)); diff --git a/airsonic-main/src/main/resources/liquibase/11.1/add-locked-column-podcast-episode.xml b/airsonic-main/src/main/resources/liquibase/11.1/add-locked-column-podcast-episode.xml new file mode 100644 index 000000000..fbfb9e2bc --- /dev/null +++ b/airsonic-main/src/main/resources/liquibase/11.1/add-locked-column-podcast-episode.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml b/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml index a480b2907..e7a516f72 100644 --- a/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml +++ b/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml @@ -7,4 +7,5 @@ + diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle.properties index c5f51d89a..0758a71e1 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle.properties @@ -803,6 +803,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Error podcastreceiver.status.deleted=Deleted podcastreceiver.status.skipped=Skipped +podcastreceiver.lockselected=Lock selected +podcastreceiver.unlockselected=Unlock selected podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Really delete podcast? @@ -819,6 +821,7 @@ podcastreceiver.categories=Categories podcastreceiver.language=Language podcastreceiver.dead=Dead podcastreceiver.locked=Locked +podcastreceiver.unlocked=Unlocked podcastreceiver.selectedchannels=Selected channels podcastreceiver.podcastindexsearch=Podcast Index Search podcastreceiver.directly=Directly Subscribe diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties index ce7e37f94..3d4bc8c65 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u0417\u0430\u0432\u044A\u0440\u0448\u0435\u043 podcastreceiver.status.error=\u0413\u0440\u0435\u0448\u043A\u0430 podcastreceiver.status.deleted=\u0418\u0437\u0442\u0440\u0438\u0442\u043E podcastreceiver.status.skipped=\u041F\u0440\u043E\u043F\u0443\u0441\u043D\u0430\u0442\u043E +podcastreceiver.lockselected=\u0417\u0430\u043A\u043B\u044E\u0447\u0438 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 +podcastreceiver.unlockselected=\u041E\u0442\u043A\u043B\u044E\u0447\u0438 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 podcastreceiver.downloadselected=\u0421\u0432\u0430\u043B\u0438 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 podcastreceiver.deleteselected=\u0418\u0437\u0442\u0440\u0438\u0439 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 podcastreceiver.confirmdelete=\u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043D\u0435 \u043D\u0430 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435? @@ -631,6 +633,8 @@ podcastreceiver.refresh=\u041F\u0440\u0435\u0437\u0430\u0440\u0435\u0434\u0438 \ podcastreceiver.settings=\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043D\u0430 \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u0430 podcastreceiver.subscribe=\u0410\u0431\u043E\u043D\u0430\u043C\u0435\u043D\u0442 \u0437\u0430 \u043F\u043E\u0434\u043A\u0430\u0441\u0442 podcastreceiver.newestepisodes=Newest episodes +podcastreceiver.locked=\u0417\u0430\u043A\u043B\u044E\u0447\u0435\u043D +podcastreceiver.unlocked=\u041E\u0442\u043A\u043B\u044E\u0447\u0435\u043D lyrics.title=\u0422\u0435\u043A\u0441\u0442 \u043D\u0430 \u043F\u0435\u0441\u0435\u043D\u0442\u0430 lyrics.artist=\u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties index b153b45c5..45d0c93e6 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties @@ -45,7 +45,7 @@ accessDenied.text=Vost\u00E8 no est\u00E0 autoritzat a realitzar aquesta operaci notFound.title=Not found notFound.text=

Sorry, could not find what you were looking for.

Try reloading the web page. If that doesn't help, try scanning the media folders again.

notFound.reload=Reload page -notFound.scan=Media folder settings +notFound.scan=Media folder settinges top.home=Inici top.now_playing=Reproduint @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Complet podcastreceiver.status.error=Error podcastreceiver.status.deleted=Esborrat podcastreceiver.status.skipped=Om\u00E8s +podcastreceiver.lockselected=Bloquejar seleccionat +podcastreceiver.unlockselected=Desbloquejar seleccionat podcastreceiver.downloadselected=Descarregar seleccionada podcastreceiver.deleteselected=Borrat seleccionat podcastreceiver.confirmdelete=Est\u00E0 segur de esborrar els Podcasts seleccionats? @@ -631,6 +633,8 @@ podcastreceiver.refresh=Actualitzar la p\u00E0gina podcastreceiver.settings=Configuraci\u00F3 dels Podcast podcastreceiver.subscribe=Subscriure's a un Podcast podcastreceiver.newestepisodes=Newest episodes +podcastreceiver.locked=Bloquejat +podcastreceiver.unlocked=Desbloquejat lyrics.title=Lletres de can\u00E7ons lyrics.artist=Artista diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties index 69ea24bd4..ccbbb48f9 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Dokon\u010Deno podcastreceiver.status.error=Chyba podcastreceiver.status.deleted=Odstran\u011Bno podcastreceiver.status.skipped=Vynech\u00E1no +podcastreceiver.lockselected=Zamknout vybran\u00E9 +podcastreceiver.unlockselected=Odemknout vybran\u00E9 podcastreceiver.downloadselected=St\u00E1hnout vybran\u00E9 podcastreceiver.deleteselected=Odstranit vybran\u00E9 podcastreceiver.confirmdelete=Opravdu odstranit vybran\u00E9 Podcasty? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties index 23e13e016..33cdfefee 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties @@ -797,6 +797,8 @@ podcastreceiver.status.completed=Afsluttet podcastreceiver.status.error=Fejl podcastreceiver.status.deleted=Slettet podcastreceiver.status.skipped=Sprunget over +podcastreceiver.lockselected=L\u00E5s valgte +podcastreceiver.unlockselected=L\u00E5s op valgte podcastreceiver.downloadselected=Download udvalgte podcastreceiver.deleteselected=Slet valgte podcastreceiver.confirmdelete=Vil du slette valgte Podcasts? @@ -812,7 +814,8 @@ podcastreceiver.nosearchresults=Ingen søgeresultater podcastreceiver.categories=Kategorier podcastreceiver.language=Sprog podcastreceiver.dead=Død -podcastreceiver.locked=Låst +podcastreceiver.locked=L\u00E5st +podcastreceiver.unlocked=L\u00E5st op podcastreceiver.selectedchannels=Udvalgte kanaler podcastreceiver.podcastindexsearch=Podcast Index Søgning podcastreceiver.directly=Tilmeld dig direkte diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties index 7d18f0901..2d1b2a2c6 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Fertig podcastreceiver.status.error=Fehler podcastreceiver.status.deleted=Gel\u00F6scht podcastreceiver.status.skipped=Abgebrochen +podcastreceiver.lockselected=Ausgew\u00E4hlte sperren +podcastreceiver.unlockselected=Ausgew\u00E4hlte freigeben podcastreceiver.downloadselected=Ausgew\u00E4hlte downloaden podcastreceiver.deleteselected=L\u00F6sche ausgew\u00E4hlte podcastreceiver.confirmdelete=Ausgew\u00E4hlte Podcasts wirklich l\u00F6schen? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties index 4eaf9e3d5..d9d491cd9 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u039F\u03BB\u03BF\u03BA\u03BB\u03B7\u03C1\u03C podcastreceiver.status.error=\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 podcastreceiver.status.deleted=\u0394\u03B9\u03B1\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF podcastreceiver.status.skipped=\u03A5\u03C0\u03B5\u03C1\u03C0\u03B7\u03B4\u03B7\u03BC\u03AD\u03BD\u03BF +podcastreceiver.unlockselected=\u0391\u03C0\u03BF\u03BA\u03BB\u03B5\u03B9\u03C3\u03BC\u03CC\u03C2 \u03C3\u03B5\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD +podcastreceiver.lockselected=\u0395\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C3\u03B5\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD podcastreceiver.downloadselected=\u039A\u03B1\u03C4\u03AD\u03B2\u03B1\u03C3\u03BC\u03B1 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD podcastreceiver.deleteselected=\u0394\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03C9\u03BD podcastreceiver.confirmdelete=\u039D\u03B1 \u03B4\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03BF\u03CD\u03BD \u03CC\u03BD\u03C4\u03C9\u03C2 \u03CC\u03BB\u03B1 \u03C4\u03B1 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03B1 Podcasts? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index 25824d200..fc5bf4f81 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -802,6 +802,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Error podcastreceiver.status.deleted=Deleted podcastreceiver.status.skipped=Skipped +podcastreceiver.lockselected=Lock selected +podcastreceiver.unlockselected=Unlock selected podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Really delete podcast? @@ -818,6 +820,7 @@ podcastreceiver.categories=Categories podcastreceiver.language=Language podcastreceiver.dead=Dead podcastreceiver.locked=Locked +podcastreceiver.unlocked=Unlocked podcastreceiver.selectedchannels=Selected channels podcastreceiver.podcastindexsearch=Podcast Index Search podcastreceiver.directly=Directly Subscribe diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties index 1c641a332..f1b8f1f91 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Error podcastreceiver.status.deleted=Deleted podcastreceiver.status.skipped=Skipped +podcastreceiver.lockselected=Lock selected +podcastreceiver.unlockselected=Unlock selected podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Really delete podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties index c4a4fc435..6787f9f76 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties @@ -624,6 +624,8 @@ podcastreceiver.status.completed=Completado podcastreceiver.status.error=Error podcastreceiver.status.deleted=Borrado podcastreceiver.status.skipped=Saltado +podcastreceiver.lockselected=Bloquear los seleccionados +podcastreceiver.unlockselected=Desbloquear los seleccionados podcastreceiver.downloadselected=Descargar los seleccionados podcastreceiver.deleteselected=Borrar los seleccionados podcastreceiver.confirmdelete=\u00BFEst\u00E1 seguro de borrar el podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties index 5061d0bc8..0f373892c 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Valmis podcastreceiver.status.error=Viga podcastreceiver.status.deleted=Kustutatud podcastreceiver.status.skipped=Vahele j\u00e4etud +podcastreceiver.lockselected=Lukustus valitud +podcastreceiver.unlockselected=Vabastuslukk valitud podcastreceiver.downloadselected=Lae alla valitud podcastreceiver.deleteselected=Kustuta valitud podcastreceiver.confirmdelete=T\u00f5esti soovite kustutada valitud taskupleierid? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties index e2f1641ff..d6fb09b93 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Valmis podcastreceiver.status.error=Virhe podcastreceiver.status.deleted=Poistettu podcastreceiver.status.skipped=Ohitettu +podcastreceiver.lockselected=Lukitus valitut +podcastreceiver.unlockselected=Vapautinlukko valitut podcastreceiver.downloadselected=Lataa valitut podcastreceiver.deleteselected=Poista valitut podcastreceiver.confirmdelete=Haluatko todella poistaa valitut podcastit? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties index 821bcc413..9189f5357 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties @@ -630,6 +630,8 @@ podcastreceiver.status.completed=Complet podcastreceiver.status.error=Erreur podcastreceiver.status.deleted=Effacer podcastreceiver.status.skipped=Passer +podcastreceiver.lockselected=Verrouiller les \u00e9missions selectionn\u00e9es +podcastreceiver.unlockselected=D\u00e9verrouiller les \u00e9missions selectionn\u00e9es podcastreceiver.downloadselected=Mettre \u00e0 jour les podcasts selectionn\u00e9s podcastreceiver.deleteselected=Effacer les \u00e9missions selectionn\u00e9es podcastreceiver.confirmdelete=Voulez-vous r\u00e9ellement effacer les \u00e9missions s\u00e9lectionn\u00e9es ? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties index cf2c516eb..89261bd89 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties @@ -623,7 +623,9 @@ podcastreceiver.status.completed=Loki\u00F0 podcastreceiver.status.error=Villa podcastreceiver.status.deleted=Eytt podcastreceiver.status.skipped=Slept -podcastreceiver.downloadselected=S\u00E6kja Vali\u00F0 +podcastreceiver.lockselected=L\u00E6sa V\u00F6ldu +podcastreceiver.unlockselected=Afl\u00E6sa V\u00F6ldu +podcastreceiver.downloadselected=S\u00E6kja V\u00F6ldu podcastreceiver.deleteselected=Ey\u00F0a V\u00F6ldu podcastreceiver.confirmdelete=Villtu ey\u00F0a V\u00F6ldu Podcasts? podcastreceiver.check=Athuga af n\u00FDjum \u00DE\u00E1ttum diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties index 7ade93a19..cbad88ea8 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completato podcastreceiver.status.error=Errore podcastreceiver.status.deleted=Eliminato podcastreceiver.status.skipped=Saltato +podcastreceiver.lockselected=Blocca selezionato +podcastreceiver.unlockselected=Sblocca selezionato podcastreceiver.downloadselected=Scarica selezionato podcastreceiver.deleteselected=Elimina selezionato podcastreceiver.confirmdelete=Cancellare davvero i Podcast selezionati? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties index ba2fa08aa..aaf2b7e44 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u5B8C\u4E86 podcastreceiver.status.error=\u30A8\u30E9\u30FC podcastreceiver.status.deleted=\u524A\u9664\u6E08\u307F podcastreceiver.status.skipped=\u30B9\u30AD\u30C3\u30D7 +podcastreceiver.lockselected=\u6307\u5B9A\u9805\u76EE\u3092\u30ED\u30C3\u30AF +podcastreceiver.unlockselected=\u6307\u5B9A\u9805\u76EE\u3092\u89E3\u9664 podcastreceiver.downloadselected=\u6307\u5B9A\u9805\u76EE\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9 podcastreceiver.deleteselected=\u6307\u5B9A\u9805\u76EE\u3092\u524A\u9664 podcastreceiver.confirmdelete=\u672C\u5F53\u306B Podcast \u3092\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F @@ -631,6 +633,8 @@ podcastreceiver.refresh=\u66F4\u65B0 podcastreceiver.settings=Podcast \u306E\u8A2D\u5B9A podcastreceiver.subscribe=Podcast \u306E\u767B\u9332 podcastreceiver.newestepisodes=Newest episodes +podcastreceiver.locked=\u30ED\u30C3\u30AF\u4E2D +podcastreceiver.unlocked=\u30ED\u30C3\u30AF\u89E3\u9664 lyrics.title=\u6B4C\u8A5E lyrics.artist=\u30A2\u30FC\u30C6\u30A3\u30B9\u30C8 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties index fc8762e83..6d9f00da0 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\uC644\uB8CC podcastreceiver.status.error=\uC624\uB958 podcastreceiver.status.deleted=\uC0AD\uC81C\uD558\uAE30 podcastreceiver.status.skipped=\uAC74\uB108\uB6F0\uAE30 +podcastreceiver.lockselected=\uC120\uD0DD \uC7A0\uAE08 +podcastreceiver.unlockselected=\uC120\uD0DD \uC7A0\uAE08 \uC81C\uAC70 podcastreceiver.downloadselected=\uC120\uD0DD \uB2E4\uC6B4\uB85C\uB4DC podcastreceiver.deleteselected=\uC120\uD0DD \uC0AD\uC81C podcastreceiver.confirmdelete=\uC120\uD0DD\uD55C \uD31F\uCE90\uC2A4\uD2B8\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties index ace50774c..eafb07f0f 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Error podcastreceiver.status.deleted=Deleted podcastreceiver.status.skipped=Skipped +podcastreceiver.lockselected=\u0418\u0437\u0431\u0440\u0430\u043d\u0430 \u0435 \u0431\u0440\u0430\u0432\u0430 +podcastreceiver.unlockselected=\u041E\u0434\u0431\u0440\u0430\u043D\u0430 \u0435 \u0431\u0440\u0430\u0432\u0430 podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Really delete podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties index 8646af58b..7fbe400f9 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Voltooid podcastreceiver.status.error=Fout podcastreceiver.status.deleted=Verwijderd podcastreceiver.status.skipped=Overgeslagen +podcastreceiver.lockselected=Selectie vergrendelen +podcastreceiver.unlockselected=Selectie ontgrendelen podcastreceiver.downloadselected=Selectie downloaden podcastreceiver.deleteselected=Selectie verwijderen podcastreceiver.confirmdelete=Weet je zeker dat je de podcast wilt verwijderen? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties index b83c52446..0778c309d 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Ferdig podcastreceiver.status.error=Feil podcastreceiver.status.deleted=Sletta podcastreceiver.status.skipped=Skippa +podcastreceiver.lockselected=L\u00E5s valde +podcastreceiver.unlockselected=L\u00E5s opp valde podcastreceiver.downloadselected=Last ned valde podcastreceiver.deleteselected=Slett valde podcastreceiver.confirmdelete=Vil du verkeleg sletta valde podcastar? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties index 80e476e23..e42fa9687 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Ferdig podcastreceiver.status.error=Feil podcastreceiver.status.deleted=Slettet podcastreceiver.status.skipped=Skippet +podcastreceiver.lockselected=L\u00E5s valgte +podcastreceiver.unlockselected=L\u00E5s opp valgte podcastreceiver.downloadselected=Last ned valgte podcastreceiver.deleteselected=Slett valgte podcastreceiver.confirmdelete=Vil du virkelig slette valgte Podcaster? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties index 7c9c7fe72..50f1c5b7a 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Uko\u0144czono podcastreceiver.status.error=B\u0142\u0105d podcastreceiver.status.deleted=Usu\u0144 podcastreceiver.status.skipped=Pomini\u0119te +podcastreceiver.lockselected=Zablokuj wybrane +podcastreceiver.unlockselected=Odblokuj wybrane podcastreceiver.downloadselected=Pobierz wybrane podcastreceiver.deleteselected=Usu\u0144 wybrane podcastreceiver.confirmdelete=Na pewno usu\u0144\u0105\u0107 wybrany Podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties index 1e0faff21..64b42b6a6 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completo podcastreceiver.status.error=Erro podcastreceiver.status.deleted=Apagado podcastreceiver.status.skipped=Ignorado +podcastreceiver.lockselected=Bloquear seleccionado +podcastreceiver.unlockselected=Desbloquear seleccionado podcastreceiver.downloadselected=Descarregar seleccionado podcastreceiver.deleteselected=Apagar seleccionado podcastreceiver.confirmdelete=Quer mesmo apagar os Podcasts seleccionados? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties index 39505a7b6..bcfb876a4 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties @@ -607,6 +607,8 @@ podcastreceiver.status.completed=Terminado podcastreceiver.status.error=Erro podcastreceiver.status.deleted=Excluído podcastreceiver.status.skipped=Pulado +podcastreceiver.lockselected=Travar selecionado +podcastreceiver.unlockselected=Desbloquear selecionado podcastreceiver.downloadselected=Download selecionado podcastreceiver.deleteselected=Excluir selecionado podcastreceiver.confirmdelete=Quer realmente excluir o podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties index 2bd3da011..0a7550e78 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Error podcastreceiver.status.deleted=Deleted podcastreceiver.status.skipped=Skipped +podcastreceiver.lockselected=Lock selected +podcastreceiver.unlockselected=Unlock selected podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Really delete podcast? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties index c8d728442..065ff89fc 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u0412\u044B\u043F\u043E\u043B\u0435\u043D\u043 podcastreceiver.status.error=\u041E\u0448\u0438\u0431\u043A\u0430 podcastreceiver.status.deleted=\u0423\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435 podcastreceiver.status.skipped=\u041F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043D\u044B\u0435 +podcastreceiver.lockselected=\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 +podcastreceiver.unlockselected=\u0420\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 podcastreceiver.downloadselected=\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 podcastreceiver.deleteselected=\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 podcastreceiver.confirmdelete=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties index bc77a81f9..5a5d75fd4 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Dokon\u010Dano podcastreceiver.status.error=Napaka podcastreceiver.status.deleted=Izbrisano podcastreceiver.status.skipped=Presko\u010Deno +podcastreceiver.lockselected=Zakleni izbrane +podcastreceiver.unlockselected=Odkleni izbrane podcastreceiver.downloadselected=Prenesi izbrane podcastreceiver.deleteselected=Izbri\u0161i izbrane podcastreceiver.confirmdelete=Resni\u010Dno \u017Eelite izbrisati izbrane podcaste? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties index acd541913..ef6e1e356 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Avslutad podcastreceiver.status.error=Error podcastreceiver.status.deleted=Borttagen podcastreceiver.status.skipped=Skippade +podcastreceiver.lockselected=L\u00E5s valda +podcastreceiver.unlockselected=L\u00F6s valda podcastreceiver.downloadselected=Ladda ner valda podcastreceiver.deleteselected=Tabort valda podcastreceiver.confirmdelete=Vill du verkligen tabort valda Podcasts? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties index 3d6e04eb0..bf2cb0dd5 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=Completed podcastreceiver.status.error=Помилка podcastreceiver.status.deleted=Видалено podcastreceiver.status.skipped=Пропущено +podcastreceiver.lockselected=Блокування вибрано +podcastreceiver.unlockselected=Розблокування вибрано podcastreceiver.downloadselected=Download selected podcastreceiver.deleteselected=Delete selected podcastreceiver.confirmdelete=Дійсно видалити подкаст? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties index a70a2ec56..7f665be75 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u5B8C\u6210 podcastreceiver.status.error=\u9519\u8BEF podcastreceiver.status.deleted=\u5DF2\u5220\u9664 podcastreceiver.status.skipped=\u7565\u8FC7 +podcastreceiver.lockselected=\u9501\u5B9A\u5DF2\u9009\u7684 +podcastreceiver.unlockselected=\u89E3\u9501\u5DF2\u9009\u7684 podcastreceiver.downloadselected=\u4E0B\u8F7D\u5DF2\u9009\u7684 podcastreceiver.deleteselected=\u5220\u9664\u5DF2\u9009\u62E9 podcastreceiver.confirmdelete=\u771F\u7684\u8981\u5220\u9664\u64AD\u5BA2? diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties index afd6cc0f8..80b622342 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties @@ -623,6 +623,8 @@ podcastreceiver.status.completed=\u5B8C\u6210 podcastreceiver.status.error=\u932F\u8AA4 podcastreceiver.status.deleted=\u5DF2\u522A\u9664 podcastreceiver.status.skipped=\u7565\u904E +podcastreceiver.lockselected=\u9396\u5B9A\u5DF2\u9078\u7684 +podcastreceiver.unlockselected=\u89E3\u9396\u5DF2\u9078\u7684 podcastreceiver.downloadselected=\u4E0B\u8F09\u5DF2\u9078\u7684 podcastreceiver.deleteselected=\u522A\u9664\u5DF2\u9078\u64C7 podcastreceiver.confirmdelete=\u771F\u7684\u8981\u522A\u9664\u64AD\u5BA2? @@ -728,4 +730,3 @@ helppopup.recaptchaSecretKey.title=reCAPTCHA secret key helppopup.recaptchaSecretKey.text=A secret key obtained from the reCAPTCHA admin console. Left unchanged if blank. helppopup.scanMediaFolders.title=Media folders scanning rules helppopup.scanMediaFolders.text=Note that subfolder names starting with a dot (.) or @eaDir, as well as Thumbs.db files, are ignored. - diff --git a/airsonic-main/src/main/resources/static/style/default-without-mediaelement.css b/airsonic-main/src/main/resources/static/style/default-without-mediaelement.css index a6dc9fa43..451ec8880 100644 --- a/airsonic-main/src/main/resources/static/style/default-without-mediaelement.css +++ b/airsonic-main/src/main/resources/static/style/default-without-mediaelement.css @@ -579,6 +579,10 @@ img { fill: currentColor; } +.feather-weak { + stroke-width: 1; +} + .feather-xs { width: 12px; height: 12px; @@ -590,6 +594,6 @@ img { } .feather-lg { - width: 32px; - height: 32px; + width: 28px; + height: 28px; } diff --git a/airsonic-main/src/main/resources/templates/podcastChannel.html b/airsonic-main/src/main/resources/templates/podcastChannel.html index 91048fd53..fa4ea4b9a 100644 --- a/airsonic-main/src/main/resources/templates/podcastChannel.html +++ b/airsonic-main/src/main/resources/templates/podcastChannel.html @@ -29,7 +29,7 @@ buttons: { /*[+ [(#{common.delete})]: function() { - location.href = "[(@{/podcastReceiverAdmin.view(channelId=${model.channel.id}, deleteChannel=${model.channel.id})})]"; + location.href = "[(@{/podcastReceiverAdmin.view(channelId=${command.channel.id}, deleteChannel=${command.channel.id})})]"; }, [(#{common.cancel})]: function() { $(this).dialog("close"); @@ -48,62 +48,60 @@ $( '#episodessa' ).click( function () { $( '.music tr input[type="checkbox"]' ).prop('checked', this.checked); + countSelected(); }); - } - function downloadSelected() { - /*[+ - location.href = "[(|@{/podcastReceiverAdmin.view(channelId=${model.channel.id})}&|)]" + - getSelectedEpisodes().map(i => "downloadEpisode=" + i).join("&"); - +]*/ - /*[-*/ - location.href = "podcastReceiverAdmin.view?channelId=0&" + - getSelectedEpisodes().map(i => "downloadEpisode=" + i).join("&"); - /*-]*/ + feather.replace(); + Array.from(document.querySelectorAll('svg.feather[title]')).forEach((element) => { + element.insertAdjacentHTML('afterbegin', `${element.attributes.title.value}`); + }); + countSelected(); } function deleteChannel() { $("#dialog-delete").dialog("open"); } - function deleteSelected() { - /*[+ - location.href = "[(|@{/podcastReceiverAdmin.view(channelId=${model.channel.id})}&|)]" + - getSelectedEpisodes().map(i => "deleteEpisode=" + i).join("&"); - +]*/ - /*[-*/ - location.href = "podcastReceiverAdmin.view?channelId=0&" + - getSelectedEpisodes().map(i => "deleteEpisode=" + i).join("&"); - /*-]*/ - } - function refreshChannels() { /*[+ - location.href = "[(@{/podcastReceiverAdmin.view(refresh,channelId=${model.channel.id})})]"; + location.href = "[(@{/podcastReceiverAdmin.view(refresh,channelId=${command.channel.id})})]"; +]*/ /*[-*/ location.href = "podcastReceiverAdmin.view?refresh&channelId=0"; /*-]*/ } - function refreshPage() { - /*[+ - location.href = "[(@{/podcastChannel.view(id=${model.channel.id})})]"; - +]*/ - /*[-*/ - location.href = "podcastChannel.view?id=0"; - /*-]*/ + function actionSelected(action) { + switch (action) { + case "download": + $('input[type=submit]#download-selected').click(); + break; + case "delete": + $('input[type=submit]#delete-selected').click(); + break; + case "lock": + $('input[type=submit]#lock-selected').click(); + break; + case "unlock": + $('input[type=submit]#unlock-selected').click(); + break; + default: + break; + } } - function getSelectedEpisodes() { - var result = []; - for (var i = 0; i < /*[(${#lists.size(model.episodes)})]*/ 3; i++) { - var checkbox = $("#episode" + i); - if (checkbox.is(":checked") && checkbox.val() && !isNaN(parseInt(checkbox.val()))) { - result.push(checkbox.val()); + function countSelected() { + var count = 0; + $('.music tr input[type="checkbox"]').each(function() { + if ($(this).prop('checked')) { + count++; } + }); + if (count == 0) { + $('#episode-control td#selected-action').hide(); + } else { + $('#episode-control td#selected-action').show(); } - return result; } @@ -114,47 +112,85 @@ .music .left { text-align: left; } + #episode-control td{ + height: 28px; + vertical-align: middle; + padding: 0 0.5em; + } + #episode-control td#selected-action{ + display: none; + } -
- +
+
-

»

+

»

- + - + | | + + | +

-
+
- – - - + – + +
+
+
+ + + + + + + + + + + + + +
+ + + +
+ - + - + + + - + @@ -162,13 +198,22 @@

- +

@@ -200,21 +245,5 @@

+ + + + + + +
- - - - - - - - - -
- - - diff --git a/airsonic-main/src/test/java/org/airsonic/player/repository/PodcastRepositoryTest.java b/airsonic-main/src/test/java/org/airsonic/player/repository/PodcastRepositoryTest.java index 3f0ef7530..89fdb1b93 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/repository/PodcastRepositoryTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/repository/PodcastRepositoryTest.java @@ -176,6 +176,31 @@ public void testGetEpisodes() { assertEpisodeEquals(d, episodes.get(3)); } + @Test + public void testGetUnlockedEpisodes() { + PodcastChannel channel = createChannel(); + PodcastEpisode a = new PodcastEpisode(null, channel, UUID.randomUUID().toString(), "a", null, null, null, + Instant.ofEpochMilli(3000), null, null, null, PodcastStatus.NEW, null); + PodcastEpisode b = new PodcastEpisode(null, channel, UUID.randomUUID().toString(), "b", null, null, null, + Instant.ofEpochMilli(1000), null, null, null, PodcastStatus.NEW, "error"); + PodcastEpisode c = new PodcastEpisode(null, channel, UUID.randomUUID().toString(), "c", null, null, null, + Instant.ofEpochMilli(2000), null, null, null, PodcastStatus.NEW, null); + PodcastEpisode d = new PodcastEpisode(null, channel, UUID.randomUUID().toString(), "c", null, null, null, + null, null, null, null, PodcastStatus.NEW, ""); + d.setLocked(true); + podcastEpisodeRepository.save(a); + podcastEpisodeRepository.save(b); + podcastEpisodeRepository.save(c); + podcastEpisodeRepository.save(d); + + channel = podcastChannelRepository.findById(channel.getId()).get(); + List episodes = podcastEpisodeRepository.findByChannelAndLockedFalse(channel); + assertEquals(3, episodes.size()); + assertEpisodeEquals(a, episodes.get(0)); + assertEpisodeEquals(b, episodes.get(1)); + assertEpisodeEquals(c, episodes.get(2)); + } + @Test public void testUpdateEpisode() { diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/PodcastPersistenceServiceTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/service/PodcastPersistenceServiceTestCase.java index 1cf13d87f..d509f784a 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/PodcastPersistenceServiceTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/PodcastPersistenceServiceTestCase.java @@ -39,6 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -50,7 +51,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -95,6 +98,8 @@ public class PodcastPersistenceServiceTestCase { private CloseableHttpResponse mockedHttpResponse; @Mock private HttpEntity mockedHttpEntity; + @Mock + private PodcastChannelRule mockedChannelRule; @TempDir private Path tempFolder; @@ -400,4 +405,82 @@ void testGetNewestEpisodes() { verify(mediaFileService, never()).getMediaFile(anyInt()); } + @Test + public void testDeleteObsoleteEpisodes() { + // Mocking the channel and episodes + when(mockedChannel.getId()).thenReturn(1); + when(mockedChannelRule.getRetentionCount()).thenReturn(5); + when(podcastRuleRepository.findById(1)).thenReturn(Optional.of(mockedChannelRule)); + + List episodes = new ArrayList<>(); + for (int i = 1; i <= 15; i++) { + episodes.add(mockedEpisode); + } + when(podcastEpisodeRepository.findByChannelAndLockedFalse(mockedChannel)).thenReturn(episodes); + + // Calling the method to be tested + podcastService.deleteObsoleteEpisodes(mockedChannel); + + // Verifying the method calls + verify(mockedEpisode, times(10)).setStatus(PodcastStatus.DELETED); + verify(mockedEpisode, times(10)).setErrorMessage(null); + verify(podcastEpisodeRepository, times(10)).save(mockedEpisode); + } + + @Test + public void testDeleteObsoleteEpisodesWithRetentionCountUnlimitedShouldDoNothing() { + // Mocking the channel and episodes + when(mockedChannel.getId()).thenReturn(1); + when(mockedChannelRule.getRetentionCount()).thenReturn(-1); + when(podcastRuleRepository.findById(1)).thenReturn(Optional.of(mockedChannelRule)); + + // Calling the method to be tested + podcastService.deleteObsoleteEpisodes(mockedChannel); + + // Verifying the method calls + verifyNoInteractions(mockedEpisode); + verifyNoInteractions(podcastEpisodeRepository); + } + + @Test + public void testDeleteObsoleteEpisodesWithDownloadingEpisodeShouldDoNothing() { + // Mocking the channel and episodes + when(mockedChannel.getId()).thenReturn(1); + when(mockedChannelRule.getRetentionCount()).thenReturn(5); + when(podcastRuleRepository.findById(1)).thenReturn(Optional.of(mockedChannelRule)); + + List episodes = new ArrayList<>(); + for (int i = 1; i <= 15; i++) { + PodcastEpisode episode = new PodcastEpisode(); + episode.setStatus(PodcastStatus.DOWNLOADING); + } + when(podcastEpisodeRepository.findByChannelAndLockedFalse(mockedChannel)).thenReturn(episodes); + + // Calling the method to be tested + podcastService.deleteObsoleteEpisodes(mockedChannel); + + // Verifying the method calls + verifyNoMoreInteractions(podcastEpisodeRepository); + } + + @Test + public void testDeleteObsoleteEpisodesWithRetentionEnoughShouldDoNothing() { + // Mocking the channel and episodes + when(mockedChannel.getId()).thenReturn(1); + when(mockedChannelRule.getRetentionCount()).thenReturn(5); + when(podcastRuleRepository.findById(1)).thenReturn(Optional.of(mockedChannelRule)); + + List episodes = new ArrayList<>(); + for (int i = 1; i <= 5; i++) { + PodcastEpisode episode = new PodcastEpisode(); + episode.setStatus(PodcastStatus.DOWNLOADING); + } + when(podcastEpisodeRepository.findByChannelAndLockedFalse(mockedChannel)).thenReturn(episodes); + + // Calling the method to be tested + podcastService.deleteObsoleteEpisodes(mockedChannel); + + // Verifying the method calls + verifyNoMoreInteractions(podcastEpisodeRepository); + } } diff --git a/docs/README.md b/docs/README.md index e47385956..2a5eefeae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,9 @@ ## Contents +- [WebUI](./webui/README.md) + - [Podcast](./webui/podcast.md) + - [Configures](./configures/README.md) - [Detail Configuration](./configures/detail.md) - Media diff --git a/docs/figures/podcast-channel.jpg b/docs/figures/podcast-channel.jpg new file mode 100644 index 000000000..1b30ebef8 Binary files /dev/null and b/docs/figures/podcast-channel.jpg differ diff --git a/docs/webui/README.md b/docs/webui/README.md new file mode 100644 index 000000000..cc11313da --- /dev/null +++ b/docs/webui/README.md @@ -0,0 +1,7 @@ +# Web UI Features + +This document describes the features of the Airsonic Advanced web UI. + +## Contents + +- [Podcast](./podcast.md) \ No newline at end of file diff --git a/docs/webui/podcast.md b/docs/webui/podcast.md new file mode 100644 index 000000000..c3f519948 --- /dev/null +++ b/docs/webui/podcast.md @@ -0,0 +1,17 @@ +# Podcast + +## Podcast Channel + +![Podcast Channel](../figures/podcast-channel.jpg) + +| Number | Description | Role | +| --- | --- | --- | +| 1 | Play the podcast channel | User | +| 2 | Delete the podcast channel | Podcast | +| 3 | Retrieve the new episode lists | Podcast | +| 4 | Configure the podcast channel rule | Admin | +| 5 | Refresh Page | User | +| 6 | Download selected episodes. This becomes visible when at least one checkbox is checked.| Podcast | +| 7 | Delete selected episodes. This becomes visible when at least one checkbox is checked. | Podcast | +| 8 | Lock selected episodes. Locked episodes will be prevented from being deleted and will not be counted in the 'Keep X latest episodes' rule. This becomes visible when at least one checkbox is checked. | Podcast | +| 9 | Unlock selected episodes. This becomes visible when at least one checkbox is checked. | Podcast |