Skip to content

Commit

Permalink
Merge pull request #493 from kagemomiji/issue315-redownload-podcast
Browse files Browse the repository at this point in the history
#315 Add feature for redownload podcast episode
  • Loading branch information
kagemomiji committed Jun 4, 2024
2 parents 885a11f + 19e76ac commit 4533ba7
Show file tree
Hide file tree
Showing 43 changed files with 694 additions and 60 deletions.
2 changes: 1 addition & 1 deletion airsonic-main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.7.2</version>
<version>2.7.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
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 2014 (C) Y.Tory
Copyright 2016 (C) Airsonic Authors
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
*/

package org.airsonic.player.controller;

import org.airsonic.player.domain.PodcastEpisode;
import org.airsonic.player.service.PodcastPersistenceService;
import org.airsonic.player.service.podcast.PodcastDownloadClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
* Controller for the "Podcast episodes"
*
* @author Y.Tory
*/
@Controller
@RequestMapping("/podcastEpisodes")
public class PodcastEpisodesControler {

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

@Autowired
private PodcastPersistenceService podcastService;

@Autowired
private PodcastDownloadClient podcastDownloadClient;

@PostMapping(params = "download")
public String downloadEpisode(
@RequestParam(name = "episodeId", required = true) Integer episodeId) throws Exception {

PodcastEpisode episode = podcastService.getEpisode(episodeId, false);
if (episode == null) {
LOG.warn("Episode {} not found", episodeId);
return "redirect:/notFound";
}

LOG.info("Downloading episode {}", episodeId);
Integer channelId = episode.getChannel().getId();
podcastDownloadClient.downloadEpisode(episodeId);

return "redirect:/podcastChannel?id=" + channelId;
}

/**
* Initialize episode
*
* @param episodeId
* @return
* @throws Exception
*/
@PostMapping(params = "init")
public String initializeEpisode(
@RequestParam(name = "episodeId", required = true) Integer episodeId) throws Exception {

PodcastEpisode episode = podcastService.getEpisode(episodeId, true);
if (episode == null) {
LOG.warn("Episode {} not found", episodeId);
return "redirect:/notFound";
}

LOG.info("Initializing episode {}", episodeId);
Integer channelId = episode.getChannel().getId();
podcastService.resetEpisode(episodeId);

return "redirect:/podcastChannel?id=" + channelId;
}

/**
* lock episode
*
* @param episodeId
* @return
* @throws Exception
*/
@PostMapping(params = "lock")
public String lockEpisode(
@RequestParam(name = "episodeId", required = true) Integer episodeId) throws Exception {

PodcastEpisode episode = podcastService.getEpisode(episodeId, true);
if (episode == null) {
LOG.warn("Episode {} not found", episodeId);
return "redirect:/notFound";
}

LOG.info("Lock episode {}", episodeId);
Integer channelId = episode.getChannel().getId();
podcastService.lockEpisode(episodeId);

return "redirect:/podcastChannel?id=" + channelId;
}

/**
* unlock episode
*
* @param episodeId
* @return
* @throws Exception
*/
@PostMapping(params = "unlock")
public String unlockEpisode(
@RequestParam(name = "episodeId", required = true) Integer episodeId) throws Exception {

PodcastEpisode episode = podcastService.getEpisode(episodeId, true);
if (episode == null) {
LOG.warn("Episode {} not found", episodeId);
return "redirect:/notFound";
}

LOG.info("Unlock episode {}", episodeId);
Integer channelId = episode.getChannel().getId();
podcastService.unlockEpisode(episodeId);

return "redirect:/podcastChannel?id=" + channelId;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ public SecurityFilterChain webSecurityFilterChain(HttpSecurity http, Authenticat
.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("/podcastReceiverAdmin*").hasRole("PODCAST")
.requestMatchers("/podcastReceiverAdmin*", "/podcastEpisodes*").hasRole("PODCAST")
.requestMatchers("/**").hasRole("USER").anyRequest().authenticated())
.formLogin((login) -> login
.loginPage("/login")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,9 @@ public PodcastChannel prepareRefreshChannel(Integer channelId) {
* @param element element to update from
* @return updated channel or original channel if element is null
*/
@Nullable
@Transactional
public PodcastChannel updateChannelByElement(PodcastChannel channel, Element element) {
public PodcastChannel updateChannelByElement(@Nonnull PodcastChannel channel, @Nullable Element element) {
if (element == null) {
return channel;
}
Expand Down Expand Up @@ -711,4 +712,29 @@ public void unlockEpisode(int episodeId) {
LOG.warn("Podcast episode with id {} not found", episodeId);
});
}


@Transactional
public void resetEpisode(Integer episodeId) {
podcastEpisodeRepository.findById(episodeId).ifPresentOrElse(episode -> {
switch (episode.getStatus()) {
case DELETED:
episode.setLocked(true); // prevent to be deleted again
episode.setStatus(PodcastStatus.NEW);
episode.setErrorMessage(null);
podcastEpisodeRepository.save(episode);
break;
case COMPLETED:
episode.setStatus(PodcastStatus.NEW);
episode.setErrorMessage(null);
podcastEpisodeRepository.save(episode);
break;
default:
LOG.warn("Episode '{}' is not in a state that can be reset", episode.getTitle());
break;
}
}, () -> {
LOG.warn("Podcast episode with id {} not found", episodeId);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -118,12 +119,18 @@ public CompletableFuture<Void> downloadEpisode(Integer episodeId) {
InputStream in = response.getEntity().getContent();
OutputStream out = new BufferedOutputStream(Files.newOutputStream(filePath))) {

// Check if the server responded with a 200 OK status code.
if (response.getStatusLine().getStatusCode() != HttpStatus.OK.value()) {
throw new IOException("Failed to download Podcast from " + episode.getUrl() + ". Status code: "
+ response.getStatusLine().getStatusCode());
}

episode.setBytesDownloaded(0L);
episode.setErrorMessage(null);
podcastPersistenceService.updateEpisode(episode);

long bytesDownloaded = 0L;
byte[] buffer = new byte[8192];
long bytesDownloaded = 0;
int n;
long nextLogCount = 30000L;

Expand All @@ -132,41 +139,45 @@ public CompletableFuture<Void> downloadEpisode(Integer episodeId) {
bytesDownloaded += n;

if (bytesDownloaded > nextLogCount) {
episode.setBytesDownloaded(bytesDownloaded);
nextLogCount += 30000L;

// Abort download if episode was deleted by user.
if (podcastPersistenceService.isEpisodeDeleted(episodeId)) {
break;
}
episode.setBytesDownloaded(bytesDownloaded);
podcastPersistenceService.updateEpisode(episode);
nextLogCount += 30000L;
}
}
episode.setBytesDownloaded(bytesDownloaded);
LOG.info("Downloaded {} bytes from Podcast {}", bytesDownloaded, episode.getUrl());
} catch (Exception x) {
LOG.warn("Failed to download Podcast from {}", episode.getUrl(), x);
episode.setStatus(PodcastStatus.ERROR);
episode.setErrorMessage(PodcastUtil.getErrorMessage(x));
podcastPersistenceService.updateEpisode(episode);
return result;
}

if (podcastPersistenceService.isEpisodeDeleted(episodeId)) {
LOG.info("Podcast {} was deleted. Aborting download.", episode.getUrl());
FileUtil.closeQuietly(out);
FileUtil.delete(filePath);
// Abort download if episode was deleted by user.
if (podcastPersistenceService.isEpisodeDeleted(episodeId)) {
LOG.info("Podcast {} was deleted. Aborting download.", episode.getUrl());
FileUtil.delete(filePath);
} else {
MediaFile file = mediaFileService.getMediaFile(relativeFile, folder);
episode.setMediaFile(file);
// Parser may not be able to determine duration for some formats.
if (file.getDuration() == null) {
String errorMessage = "Failed to get duration for " + file;
LOG.warn(errorMessage);
episode.setStatus(PodcastStatus.ERROR);
episode.setErrorMessage(errorMessage);
podcastPersistenceService.updateEpisode(episode);
} else {
FileUtil.closeQuietly(out);
episode.setBytesDownloaded(bytesDownloaded);
LOG.info("Downloaded {} bytes from Podcast {}", bytesDownloaded, episode.getUrl());
MediaFile file = mediaFileService.getMediaFile(relativeFile, folder);
episode.setMediaFile(file);
// Parser may not be able to determine duration for some formats.
if (file.getDuration() == null) {
throw new RuntimeException("Failed to get duration for " + file);
}
updateTags(file, episode);
episode.setStatus(PodcastStatus.COMPLETED);
podcastPersistenceService.updateEpisode(episode);
podcastPersistenceService.deleteObsoleteEpisodes(channel);
}
} catch (Exception x) {
LOG.warn("Failed to download Podcast from {}", episode.getUrl(), x);
episode.setStatus(PodcastStatus.ERROR);
episode.setErrorMessage(PodcastUtil.getErrorMessage(x));
podcastPersistenceService.updateEpisode(episode);
}
} else {
LOG.info("Episode with id {} not found", episodeId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ podcastreceiver.deleteselected=Delete selected
podcastreceiver.confirmdelete=Really delete podcast?
podcastreceiver.check=Check for new episodes
podcastreceiver.refresh=Refresh page
podcastreceiver.reset=Reset
podcastreceiver.settings=Podcast settings
podcastreceiver.subscribe=Subscribe to podcasts
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=\u0418\u0437\u0442\u0440\u0438\u0439 \u0438\u0437
podcastreceiver.confirmdelete=\u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043D\u0435 \u043D\u0430 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435?
podcastreceiver.check=\u041F\u0440\u043E\u0432\u0435\u0440\u0438 \u0437\u0430 \u043D\u043E\u0432\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438
podcastreceiver.refresh=\u041F\u0440\u0435\u0437\u0430\u0440\u0435\u0434\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430\u0442\u0430
podcastreceiver.reset=\u0418\u0437\u0447\u0438\u0441\u0442\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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=Borrat seleccionat
podcastreceiver.confirmdelete=Est\u00E0 segur de esborrar els Podcasts seleccionats?
podcastreceiver.check=Comprovar nous cap\u00EDtols
podcastreceiver.refresh=Actualitzar la p\u00E0gina
podcastreceiver.reset=Reiniciar
podcastreceiver.settings=Configuraci\u00F3 dels Podcast
podcastreceiver.subscribe=Subscriure's a un Podcast
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=Odstranit vybran\u00E9
podcastreceiver.confirmdelete=Opravdu odstranit vybran\u00E9 Podcasty?
podcastreceiver.check=Zkontrolovat nov\u00E9 epizody
podcastreceiver.refresh=Obnovit str\u00E1nku
podcastreceiver.reset=Resetovat
podcastreceiver.settings=Nastaven\u00ED Podsatu
podcastreceiver.subscribe=P\u0159ihl\u00E1sit se k odb\u011Bru Podcastu
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ podcastreceiver.deleteselected=Slet valgte
podcastreceiver.confirmdelete=Vil du slette valgte Podcasts?
podcastreceiver.check=Kontroller for nye episoder
podcastreceiver.refresh=Opdater side
podcastreceiver.reset=Nulstil
podcastreceiver.settings=Podcast indstillinger
podcastreceiver.subscribe=Abonner p\u00E5 podcast
podcastreceiver.newestepisodes=Nyeste episoder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=L\u00F6sche ausgew\u00E4hlte
podcastreceiver.confirmdelete=Ausgew\u00E4hlte Podcasts wirklich l\u00F6schen?
podcastreceiver.check=Suche nach neuen Episoden
podcastreceiver.refresh=Seite neu laden
podcastreceiver.reset=Zur\u00FCcksetzen
podcastreceiver.settings=Podcast Einstellungen
podcastreceiver.subscribe=Abonniere Podcast
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=\u0394\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE
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?
podcastreceiver.check=\u0388\u03BB\u03B5\u03B3\u03C7\u03BF\u03C2 \u03B3\u03B9\u03B1 \u03BA\u03B1\u03B9\u03BD\u03BF\u03CD\u03C1\u03B9\u03B1 \u03B5\u03C0\u03B5\u03B9\u03C3\u03CC\u03B4\u03B9\u03B1
podcastreceiver.refresh=\u0391\u03BD\u03B1\u03BD\u03AD\u03C9\u03C3\u03B7 \u03C3\u03B5\u03BB\u03AF\u03B4\u03B1\u03C2
podcastreceiver.reset=\u0395\u03C0\u03B1\u03BD\u03B1\u03C6\u03BF\u03C1\u03AC
podcastreceiver.settings=Podcast \u03C1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2
podcastreceiver.subscribe=\u0393\u03AF\u03BD\u03B5\u03C4\u03B5 \u03C3\u03C5\u03BD\u03B4\u03C1\u03BF\u03BC\u03B7\u03C4\u03AD\u03C2 \u03C3\u03B5 Podcast
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ podcastreceiver.deleteselected=Delete selected
podcastreceiver.confirmdelete=Really delete podcast?
podcastreceiver.check=Check for new episodes
podcastreceiver.refresh=Refresh page
podcastreceiver.reset=Reset
podcastreceiver.settings=Podcast settings
podcastreceiver.subscribe=Subscribe to podcasts
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=Delete selected
podcastreceiver.confirmdelete=Really delete podcast?
podcastreceiver.check=Check for new episodes
podcastreceiver.refresh=Refresh page
podcastreceiver.reset=Reset
podcastreceiver.settings=Podcast settings
podcastreceiver.subscribe=Subscribe to podcast
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ edittags.year=A\u00F1o
edittags.genre=G\u00E9nero
edittags.status=Estado
edittags.suggest=Sugerir
edittags.reset=Reset
edittags.reset=Restablecer
edittags.suggest.short=S
edittags.reset.short=R
edittags.set=Establecer
Expand Down Expand Up @@ -631,6 +631,7 @@ podcastreceiver.deleteselected=Borrar los seleccionados
podcastreceiver.confirmdelete=\u00BFEst\u00E1 seguro de borrar el podcast?
podcastreceiver.check=Comprobar si hay nuevos episodios
podcastreceiver.refresh=Recargar la p\u00E1gina
podcastreceiver.reset=Restablecer
podcastreceiver.settings=Ajustes de podcasts
podcastreceiver.subscribe=Suscribirse al podcast
podcastreceiver.newestepisodes=Episodios m\u00E1s nuevos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ podcastreceiver.deleteselected=Kustuta valitud
podcastreceiver.confirmdelete=T\u00f5esti soovite kustutada valitud taskupleierid?
podcastreceiver.check=Kontrolli uute osade saadavust
podcastreceiver.refresh=V\u00e4rskenda lehek\u00fclge
podcastreceiver.reset=Taasv\u00e4rtusta
podcastreceiver.settings=Taskupleieri seaded
podcastreceiver.subscribe=Telli taskupleier
podcastreceiver.newestepisodes=Newest episodes
Expand Down
Loading

0 comments on commit 4533ba7

Please sign in to comment.