#5 Playlist Editor With Radio Reference Support (#760)

* WIP - Adds support for aliasing P25 Phase 2 tones.Playlist editor - adds editor support for alias actions

* Playlist editor implementation and integration into application is complete. Adds JavaFX window monitoring to persist window size/location across application runs.

Co-authored-by: Denny <denny@denny-desktop>
This commit is contained in:
Denny Sheirer
2020-04-26 11:53:43 +00:00
committed by GitHub
parent 39f6d6c831
commit cc52ae5bd7
290 changed files with 31251 additions and 15306 deletions

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 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
@@ -25,18 +25,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.github.dsheirer.alias.AliasEvent;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.map.ChannelMapEvent;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.playlist.PlaylistPreference;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.service.radioreference.RadioReference;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.tuner.TunerModel;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,10 +48,15 @@ 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.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages all aspects of playlists and related models
*/
public class PlaylistManager implements Listener<ChannelEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(PlaylistManager.class);
@@ -56,81 +64,139 @@ public class PlaylistManager implements Listener<ChannelEvent>
public static final int PLAYLIST_CURRENT_VERSION = 4;
private AliasModel mAliasModel;
private ChannelMapModel mChannelMapModel = new ChannelMapModel();
private IconManager mIconManager;
private BroadcastModel mBroadcastModel;
private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel;
private ChannelProcessingManager mChannelProcessingManager;
private SourceManager mSourceManager;
private UserPreferences mUserPreferences;
private RadioReference mRadioReference;
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private ScheduledFuture<?> mPlaylistSaveFuture;
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
* Playlist manager - manages all channel configurations, channel maps, and alias lists and handles loading or
* persisting to the current playlist file
*
* Monitors playlist changes to automatically save configuration changes
* after they occur.
* Monitors playlist changes to automatically save configuration changes after they occur.
*
* @param channelModel
* @param userPreferences for user settings
* @param sourceManager for access to tuner model
* @param eventLogManager for event logging
*/
public PlaylistManager(AliasModel aliasModel, BroadcastModel broadcastModel, ChannelModel channelModel,
ChannelMapModel channelMapModel, UserPreferences userPreferences)
public PlaylistManager(UserPreferences userPreferences, SourceManager sourceManager, AliasModel aliasModel,
EventLogManager eventLogManager, IconManager iconManager)
{
mAliasModel = aliasModel;
mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
mUserPreferences = userPreferences;
mSourceManager = sourceManager;
mAliasModel = aliasModel;
mIconManager = iconManager;
mBroadcastModel = new BroadcastModel(mAliasModel, mIconManager, userPreferences);
mRadioReference = new RadioReference(mUserPreferences);
mChannelModel = new ChannelModel(mAliasModel);
mChannelProcessingManager = new ChannelProcessingManager(mChannelMapModel, eventLogManager, mSourceManager,
mAliasModel, mUserPreferences);
mChannelModel.addListener(mChannelProcessingManager);
mChannelProcessingManager.addChannelEventListener(mChannelModel);
//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();
}
mAliasModel.addListener(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();
}
mChannelMapModel.addListener(t -> {
//Save the playlist for all channel map events
schedulePlaylistSave();
});
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
mBroadcastModel.addListener(broadcastEvent -> {
switch(broadcastEvent.getEvent())
{
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;
}
case CONFIGURATION_ADD:
case CONFIGURATION_CHANGE:
case CONFIGURATION_DELETE:
schedulePlaylistSave();
break;
default:
//Do nothing
break;
}
});
}
/**
* Channel model managed by this playlist manager
*/
public ChannelModel getChannelModel()
{
return mChannelModel;
}
/**
* Channel processing manager
*/
public ChannelProcessingManager getChannelProcessingManager()
{
return mChannelProcessingManager;
}
/**
* Alias model managed by this playlist manager
*/
public AliasModel getAliasModel()
{
return mAliasModel;
}
/**
* Icon manager
*/
public IconManager getIconManager()
{
return mIconManager;
}
/**
* Radio Reference service interface
*/
public RadioReference getRadioReference()
{
return mRadioReference;
}
/**
* Audio Broadcast (streaming) Model
*/
public BroadcastModel getBroadcastModel()
{
return mBroadcastModel;
}
/**
* Channel Map model managed by this playlist manager
*/
public ChannelMapModel getChannelMapModel()
{
return mChannelMapModel;
}
/**
* Tuner model for this playlist manager
*/
public TunerModel getTunerModel()
{
return mSourceManager.getTunerModel();
}
/**
* Loads playlist from the current playlist file, or the default playlist file,
* as specified in the current SDRTRunk system settings
@@ -141,6 +207,128 @@ public class PlaylistManager implements Listener<ChannelEvent>
transferPlaylistToModels(playlist);
}
/**
* Closes the current playlist, saving if necessary, clears the models and sets the current playlist to use
* the specified playlist path.
* @param path to use for the playlist.
*/
public void setPlaylist(Path path) throws IOException
{
if(path == null)
{
throw new IllegalArgumentException("Specified playlist path does not exist");
}
//Complete any pending playlist save
saveNow();
mUserPreferences.getPlaylistPreference().setPlaylist(path);
if(!Files.exists(path))
{
createEmptyPlaylist(path);
}
init();
}
/**
* Checks the path argument to determine if it is a valid V2 playlist file by loading and deserializer the file.
* @param path to check
* @return true if the path is a valid playist.
*/
public static boolean isPlaylist(Path path)
{
if(path == null || !Files.exists(path))
{
return false;
}
JacksonXmlModule xmlModule = new JacksonXmlModule();
xmlModule.setDefaultUseWrapper(false);
ObjectMapper objectMapper = new XmlMapper(xmlModule);
try(InputStream in = Files.newInputStream(path))
{
PlaylistV2 playlist = objectMapper.readValue(in, PlaylistV2.class);
//If jackson can successfully deserialize the file, then it's a good V2 playlist
return true;
}
catch(IOException ioe)
{
mLog.error("IO error while reading playlist file", ioe);
}
return false;
}
/**
* Creates an empty serialized playlist
* @param path for storing the playlist
* @throws IOException if there is an error writing to disk
*/
public void createEmptyPlaylist(Path path) throws IOException
{
PlaylistV2 playlist = new PlaylistV2();
try(OutputStream out = Files.newOutputStream(path))
{
JacksonXmlModule xmlModule = new JacksonXmlModule();
xmlModule.setDefaultUseWrapper(false);
ObjectMapper objectMapper = new XmlMapper(xmlModule);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.writeValue(out, playlist);
out.flush();
}
catch(IOException ioe)
{
throw ioe;
}
catch(Exception e)
{
mLog.error("Error while creating empty playlist [" + path.toString() + "]", e);
}
}
private void clearModels()
{
mPlaylistLoading = true;
//Shutdown any running channels
mChannelProcessingManager.shutdown();
mChannelModel.clear();
mChannelMapModel.clear();
mBroadcastModel.clear();
mAliasModel.clear();
mPlaylistLoading = false;
}
private void saveNow()
{
//Complete any pending playlist save
if(mPlaylistSaveFuture != null)
{
try
{
mPlaylistSaveFuture.cancel(true);
}
catch(Exception e)
{
mLog.error("Error trying to cancel pending playlist save");
}
mPlaylistSaveFuture = null;
}
if(mPlaylistSavePending.getAndSet(false))
{
save();
}
}
/**
* Transfers data from persisted playlist into system models
*/
@@ -148,10 +336,15 @@ public class PlaylistManager implements Listener<ChannelEvent>
{
if(playlist != null)
{
clearModels();
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
@@ -161,7 +354,6 @@ public class PlaylistManager implements Listener<ChannelEvent>
}
}
/**
* Channel event listener method. Monitors channel events for events that indicate that the playlist has changed
* and queues automatic playlist saving.
@@ -188,7 +380,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
*/
private void save()
{
PlaylistPreference files = mUserPreferences.getPlaylistPreference();
PlaylistPreference playlistPreference = mUserPreferences.getPlaylistPreference();
PlaylistV2 playlist = new PlaylistV2();
@@ -199,34 +391,35 @@ public class PlaylistManager implements Listener<ChannelEvent>
playlist.setVersion(PLAYLIST_CURRENT_VERSION);
//Create a backup copy of the current playlist
if(Files.exists(files.getPlaylist()))
if(Files.exists(playlistPreference.getPlaylist()))
{
try
{
Files.copy(files.getPlaylist(), files.getPlaylistBackup(), StandardCopyOption.REPLACE_EXISTING);
Files.copy(playlistPreference.getPlaylist(), playlistPreference.getPlaylistBackup(),
StandardCopyOption.REPLACE_EXISTING);
}
catch(Exception e)
{
mLog.error("Error creating backup copy of current playlist prior to saving updates [" +
files.getPlaylist().toString() + "]", e);
playlistPreference.getPlaylist().toString() + "]", e);
}
}
//Create a temporary lock file to signify that we're in the process of updating the playlist
if(!Files.exists(files.getPlaylistLock()))
if(!Files.exists(playlistPreference.getPlaylistLock()))
{
try
{
Files.createFile(files.getPlaylistLock());
Files.createFile(playlistPreference.getPlaylistLock());
}
catch(IOException e)
{
mLog.error("Error creating temporary lock file prior to saving playlist [" +
files.getPlaylistLock().toString() + "]", e);
playlistPreference.getPlaylistLock().toString() + "]", e);
}
}
try(OutputStream out = Files.newOutputStream(files.getPlaylist()))
try(OutputStream out = Files.newOutputStream(playlistPreference.getPlaylist()))
{
JacksonXmlModule xmlModule = new JacksonXmlModule();
xmlModule.setDefaultUseWrapper(false);
@@ -236,20 +429,18 @@ public class PlaylistManager implements Listener<ChannelEvent>
out.flush();
//Remove the playlist lock file to indicate that we successfully saved the file
if(Files.exists(files.getPlaylistLock()))
if(Files.exists(playlistPreference.getPlaylistLock()))
{
Files.delete(files.getPlaylistLock());
Files.delete(playlistPreference.getPlaylistLock());
}
mUserPreferences.getPlaylistPreference().setPlaylistLastAccessedPath(files.getPlaylist());
}
catch(IOException ioe)
{
mLog.error("IO error while writing the playlist to a file [" + files.getPlaylist().toString() + "]", ioe);
mLog.error("IO error while writing the playlist to a file [" + playlistPreference.getPlaylist().toString() + "]", ioe);
}
catch(Exception e)
{
mLog.error("Error while saving playlist [" + files.getPlaylist().toString() + "]", e);
mLog.error("Error while saving playlist [" + playlistPreference.getPlaylist().toString() + "]", e);
}
}
@@ -290,7 +481,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
if(Files.exists(files.getPlaylist()))
{
mLog.info("Loading playlist file [" + files.getPlaylist().toString() + "]");
mLog.info("Loading playlist [" + files.getPlaylist().toString() + "]");
JacksonXmlModule xmlModule = new JacksonXmlModule();
xmlModule.setDefaultUseWrapper(false);
@@ -312,7 +503,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
}
else if(Files.exists(files.getLegacyPlaylist()))
{
mLog.info("Loading legacy playlist file [" + files.getLegacyPlaylist().toString() + "]");
mLog.info("Loading legacy playlist [" + files.getLegacyPlaylist().toString() + "]");
JacksonXmlModule xmlModule = new JacksonXmlModule();
xmlModule.setDefaultUseWrapper(false);
@@ -358,7 +549,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
{
if(mPlaylistSavePending.compareAndSet(false, true))
{
ThreadPool.SCHEDULED.schedule(new PlaylistSaveTask(), 2, TimeUnit.SECONDS);
mPlaylistSaveFuture = ThreadPool.SCHEDULED.schedule(new PlaylistSaveTask(), 2, TimeUnit.SECONDS);
}
}
}
@@ -373,6 +564,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
{
save();
mPlaylistSaveFuture = null;
mPlaylistSavePending.set(false);
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 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
@@ -116,7 +116,7 @@ public class PlaylistUpdater
for(Alias alias: playlist.getAliases())
{
Iterator<AliasID> it = alias.getId().iterator();
Iterator<AliasID> it = alias.getAliasIdentifiers().iterator();
List<AliasID> convertedAliasIds = new ArrayList<>();
@@ -258,7 +258,7 @@ public class PlaylistUpdater
for(Alias alias : playlist.getAliases())
{
Iterator<AliasID> it = alias.getId().iterator();
Iterator<AliasID> it = alias.getAliasIdentifiers().iterator();
while(it.hasNext())
{
@@ -285,7 +285,7 @@ public class PlaylistUpdater
for(Alias alias : playlist.getAliases())
{
Iterator<AliasID> it = alias.getId().iterator();
Iterator<AliasID> it = alias.getAliasIdentifiers().iterator();
while(it.hasNext())
{
@@ -319,7 +319,7 @@ public class PlaylistUpdater
for(Alias alias : playlist.getAliases())
{
Iterator<AliasID> it = alias.getId().iterator();
Iterator<AliasID> it = alias.getAliasIdentifiers().iterator();
while(it.hasNext())
{

View File

@@ -1,20 +1,24 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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.
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
*
* 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 io.github.dsheirer.playlist;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@@ -30,7 +34,7 @@ import java.util.List;
@JacksonXmlRootElement(localName = "playlist")
public class PlaylistV2
{
private int mVersion = 1;
private int mVersion = PlaylistManager.PLAYLIST_CURRENT_VERSION;
private List<Alias> mAliases = new ArrayList<>();
private List<BroadcastConfiguration> mBroadcastConfigurations = new ArrayList<>();
private List<Channel> mChannels = new ArrayList<>();