/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package playlist;
import alias.AliasEvent;
import alias.AliasModel;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import controller.channel.Channel.ChannelType;
import controller.channel.ChannelEvent;
import controller.channel.ChannelEventListener;
import controller.channel.ChannelModel;
import controller.channel.map.ChannelMapEvent;
import controller.channel.map.ChannelMapModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import playlist.version1.PlaylistConverterV1ToV2;
import properties.SystemProperties;
import sample.Listener;
import util.ThreadPool;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class PlaylistManager implements ChannelEventListener
{
private final static Logger mLog = LoggerFactory.getLogger(PlaylistManager.class);
private AliasModel mAliasModel;
private BroadcastModel mBroadcastModel;
private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel;
private Path mPlaylistFolderPath;
private Path mPlaylistCurrentPath;
private Path mPlaylistBackupPath;
private Path mPlaylistLockPath;
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private boolean mPlaylistLoading = false;
/**
* Playlist manager - manages all channel configurations, channel maps, and
* alias lists and handles loading or persisting to a playlist.xml file
*
* Monitors playlist changes to automatically save configuration changes
* after they occur.
*
* @param channelModel
*/
public PlaylistManager(AliasModel aliasModel, BroadcastModel broadcastModel, ChannelModel channelModel, ChannelMapModel channelMapModel)
{
mAliasModel = aliasModel;
mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
//Register for alias, channel and channel map events so that we can
//save the playlist when there are any changes
mChannelModel.addListener(this);
mAliasModel.addListener(new Listener<AliasEvent>()
{
@Override
public void receive(AliasEvent t)
{
//Save the playlist for all alias events
schedulePlaylistSave();
}
});
mChannelMapModel.addListener(new Listener<ChannelMapEvent>()
{
@Override
public void receive(ChannelMapEvent t)
{
//Save the playlist for all channel map events
schedulePlaylistSave();
}
});
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
switch(broadcastEvent.getEvent())
{
case CONFIGURATION_ADD:
case CONFIGURATION_CHANGE:
case CONFIGURATION_DELETE:
schedulePlaylistSave();
break;
case BROADCASTER_ADD:
case BROADCASTER_QUEUE_CHANGE:
case BROADCASTER_STATE_CHANGE:
case BROADCASTER_STREAMED_COUNT_CHANGE:
case BROADCASTER_AGED_OFF_COUNT_CHANGE:
case BROADCASTER_DELETE:
default:
//Do nothing
break;
}
}
});
}
/**
* Loads playlist from the current playlist file, or the default playlist file,
* as specified in the current SDRTRunk system settings
*/
public void init()
{
PlaylistV2 playlist = load();
boolean saveRequired = false;
if(playlist == null)
{
mLog.info("Couldn't find version 2 playlist - looking for version 1 playlist to convert");
Path playlistV1Path = getPlaylistFolderPath().resolve("playlist.xml");
PlaylistConverterV1ToV2 converter = new PlaylistConverterV1ToV2(playlistV1Path);
if(converter.hasErrorMessages())
{
mLog.error("Playlist version 1 conversion errors: " + converter.getErrorMessages());
}
playlist = converter.getConvertedPlaylist();
saveRequired = true;
}
transferPlaylistToModels(playlist);
if(saveRequired)
{
schedulePlaylistSave();
}
}
/**
* Transfers data from persisted playlist into system models
*/
private void transferPlaylistToModels(PlaylistV2 playlist)
{
if(playlist != null)
{
mPlaylistLoading = true;
mAliasModel.addAliases(playlist.getAliases());
mBroadcastModel.addBroadcastConfigurations(playlist.getBroadcastConfigurations());
mChannelMapModel.addChannelMaps(playlist.getChannelMaps());
//Channel model has to be loaded last since it will auto-start channels that are enabled
mChannelModel.addChannels(playlist.getChannels());
mPlaylistLoading = false;
}
}
/**
* Channel event listener method. Monitors channel events for events that indicate that the playlist has changed
* and queues automatic playlist saving.
*/
@Override
public void channelChanged(ChannelEvent event)
{
//Only save playlist for changes to standard channels (not traffic)
if(event.getChannel().getChannelType() == ChannelType.STANDARD)
{
switch(event.getEvent())
{
case NOTIFICATION_ADD:
case NOTIFICATION_CONFIGURATION_CHANGE:
case NOTIFICATION_DELETE:
schedulePlaylistSave();
break;
}
}
}
/**
* Folder where playlist, backup and lock file are stored
*/
private Path getPlaylistFolderPath()
{
if(mPlaylistFolderPath == null)
{
SystemProperties props = SystemProperties.getInstance();
mPlaylistFolderPath = props.getApplicationFolder("playlist");
}
return mPlaylistFolderPath;
}
/**
* Path to current playlist
*/
private Path getPlaylistPath()
{
if(mPlaylistCurrentPath == null)
{
//Temporarily removed and hard-coding the V2 playlist name. Will be reimplemented with future
//enhancement that allows multiple playlists.
// SystemProperties props = SystemProperties.getInstance();
// String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
// String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
mPlaylistCurrentPath = getPlaylistFolderPath().resolve("playlist_v2.xml");
}
return mPlaylistCurrentPath;
}
/**
* Path to most recent playlist backup
*/
private Path getPlaylistBackupPath()
{
if(mPlaylistBackupPath == null)
{
SystemProperties props = SystemProperties.getInstance();
String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
String playlistBackup = playlistCurrent.replace(".xml", ".bak");
mPlaylistBackupPath = getPlaylistFolderPath().resolve(playlistBackup);
}
return mPlaylistBackupPath;
}
/**
* Path to playlist lock file that is created prior to saving a playlist and removed immediately thereafter.
* Presence of a lock file indicates an incomplete or corrupt playlist file on startup.
*/
private Path getPlaylistLockPath()
{
if(mPlaylistLockPath == null)
{
SystemProperties props = SystemProperties.getInstance();
String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
String playlistLock = playlistCurrent.replace(".xml", ".lock");
mPlaylistLockPath = getPlaylistFolderPath().resolve(playlistLock);
}
return mPlaylistLockPath;
}
/**
* Saves the current playlist
*/
private void save()
{
PlaylistV2 playlist = new PlaylistV2();
playlist.setAliases(mAliasModel.getAliases());
playlist.setBroadcastConfigurations(mBroadcastModel.getBroadcastConfigurations());
playlist.setChannels(mChannelModel.getChannels());
playlist.setChannelMaps(mChannelMapModel.getChannelMaps());
JAXBContext context = null;
//Create a backup copy of the current playlist
if(Files.exists(getPlaylistPath()))
{
try
{
Files.copy(getPlaylistPath(), getPlaylistBackupPath(), StandardCopyOption.REPLACE_EXISTING);
}
catch(Exception e)
{
mLog.error("Error creating backup copy of current playlist prior to saving updates [" +
getPlaylistPath().toString() + "]", e);
}
}
//Create a temporary lock file to signify that we're in the process of updating the playlist
if(!Files.exists(getPlaylistLockPath()))
{
try
{
Files.createFile(getPlaylistLockPath());
}
catch(IOException e)
{
mLog.error("Error creating temporary lock file prior to saving playlist [" +
getPlaylistLockPath().toString() + "]", e);
}
}
try(OutputStream out = Files.newOutputStream(getPlaylistPath()))
{
context = JAXBContext.newInstance(PlaylistV2.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.marshal(playlist, out);
out.flush();
//Remove the playlist lock file to indicate that we successfully saved the file
if(Files.exists(getPlaylistLockPath()))
{
Files.delete(getPlaylistLockPath());
}
}
catch(JAXBException je)
{
mLog.error("JAXB exception while serializing the playlist to a file [" + getPlaylistPath().toString() +
"]", je);
}
catch(IOException ioe)
{
mLog.error("IO error while writing the playlist to a file [" + getPlaylistPath().toString() + "]", ioe);
}
catch(Exception e)
{
mLog.error("Error while saving playlist [" + getPlaylistPath().toString() + "]", e);
}
}
/**
* Loads a version 2 playlist
*/
public PlaylistV2 load()
{
mLog.info("Attempting to load version 2 playlist file [" + getPlaylistPath().toString() + "]");
PlaylistV2 playlist = null;
//Check for a lock file that indicates the previous save attempt was incomplete or had an error
if(Files.exists(getPlaylistLockPath()))
{
try
{
//Remove the previous playlist
Files.delete(getPlaylistPath());
//Copy the backup file to restore the previous playlist
if(Files.exists(getPlaylistBackupPath()))
{
Files.copy(getPlaylistBackupPath(), getPlaylistPath());
}
//Remove the lock file
Files.delete(getPlaylistLockPath());
}
catch(IOException ioe)
{
mLog.error("Previous playlist save attempt was incomplete and there was an error restoring the " +
"playlist backup file", ioe);
}
}
if(Files.exists(getPlaylistPath()))
{
JAXBContext context = null;
try(InputStream in = Files.newInputStream(getPlaylistPath()))
{
context = JAXBContext.newInstance(PlaylistV2.class);
Unmarshaller m = context.createUnmarshaller();
playlist = (PlaylistV2) m.unmarshal(in);
}
catch(JAXBException je)
{
mLog.error("JAXB exception while loading/unmarshalling playlist", je);
}
catch(IOException ioe)
{
mLog.error("IO error while reading playlist file", ioe);
}
}
else
{
mLog.info("PlaylistManager - playlist not found at [" + getPlaylistPath().toString() + "]");
}
return playlist;
}
/**
* Schedules a playlist save task. Subsequent calls to this method will be ignored until the save event occurs,
* thus limiting repetitive playlist saving to a minimum.
*/
private void schedulePlaylistSave()
{
if(!mPlaylistLoading)
{
if(mPlaylistSavePending.compareAndSet(false, true))
{
ThreadPool.SCHEDULED.schedule(new PlaylistSaveTask(), 2, TimeUnit.SECONDS);
}
}
}
/**
* Resets the playlist save pending flag to false and proceeds to save the playlist.
*/
public class PlaylistSaveTask implements Runnable
{
@Override
public void run()
{
save();
mPlaylistSavePending.set(false);
}
}
}