Somebody’s watching me nowplaying

Ik heb een nowplaying convertor geschreven. GML kan verschillende NowPlaying bestanden genereren. Wat ik nodig heb is platte tekst met “{artiest} – {titel}” voor mijn stream naar de zender. Mijn doel is om het nowplaying bestand om te kunnen zetten naar platte tekst, op het moment dat ze wijzigen omdat er een nieuwe track wordt gestart. Coding a gogo!

Ik kan om de paar seconde kijken of het bestand is veranderd, maar in dotNet bestaat ook een prachtige FileSystemWatcher die ik daarvoor geschikt acht. Deze houdt een directory of in mijn geval het specifieke bestand in de gaten dat ik opgeef. Als daar een bestaand bestand verandert roept ie een Change-event aan: dat is een method die ik moet schrijven om mijn omzetting uit te voeren en mijn nieuwe NowPlaying-bestandje weg te schrijven.

FileSystemWatcher

Ik begin met een console, die een Execute() method in mijn NowPlayingFileWatcher class aanroept:

INowPlayingFileWatcher nowPlayingFileWatcher = new NowPlayingFileWatcher();
nowPlayingFileWatcher.Execute();

De NowPlayingFileWatcher moet maar 2 dingen doen: bij de opstart de FileSystemWatcher instellen, en de method voor de Chang-event afhandeling.

De eerste taak:

public void Execute()
{
   if (!Directory.Exists(nowPlayingPath))
   {
      throw new ApplicationException(nowPlayingPath + " niet gevonden");
   }

   fw = new FileSystemWatcher(owPlayingPath);
   fw.Changed += fw_Changed;
   fw.Filter = nowPlayingFileName;
   fw.NotifyFilter = NotifyFilters.LastWrite;
   fw.EnableRaisingEvents = true;

   new System.Threading.AutoResetEvent(false).WaitOne();
}

En de 2e taak:

private void fw_Changed(object sender, FileSystemEventArgs e)
{
   if (e.ChangeType != WatcherChangeTypes.Changed) return;

  //converteer gegevens
}

De FileSystemWatcher roept de eventhandler “fw_Changed” aan en geeft daarbij ook FileSystemEventArgs mee. Hier zit de ChangeType: je kan hem ook laten reageren op Create, Rename of Delete, maar voor de zekerheid negeer ik alles wat geen WatcherChangeTypes.Changed is. De code die de taak uitvoert doet bewaar ik voor het laatst.

Implementaties

Op dit moment wordt er naar 3 nowplaying-formaten geschreven:

  • OrbanPad Serial/ Ascii Text formaat: een bestand met één regel, waarin velden worden gescheiden met een tilde (‘~’) en bij ieder veld de key value wordt gescheiden door een ‘=’-teken
  • Dalet XML: een XML bestand met de node BroadcastMonitor
  • DaletSender, een tekstbestand met 5 regels, eerste is artiest en tweede is titel, de rest kan ook gevuld worden maar is in mijn geval leeg

Ik heb maar formaat nodig, maar ik wil wel flexibel zijn, dus ga ik voor alle drie bronbestandformaten een methode bedenken om de informatie eruit te halen: de implementatie.  De uiteindelijke informatie ga ik niet in die implementatie naar mijn nowplayingbestand wegschrijven, maar geef ik terug in een ArtistTitleModel, zodat ik ergens ander kan bepalen hoe ik deze informatie afhandel:

public class ArtistTitleModel
{
   public string Artist { get; set; }
   public string Title { get; set; }
}

Iedere implementatie baseer ik op dezelfde interface:

public interface IExtract
{
   string FullPath { set; }    // stel het bron bestand in
   void GetNowPlayingData();   // lees het bron bestand
   ArtistTitleModel Extract(); // geef de informatie als ArtistTitleModel
}

NullExtractor

Iedere implementatie heeft zijn eigen interface, die de IExtract implementeert. Ik begin met een ‘extra’ implementatie: de NullExtractor. Deze doet niets, maar is voor als het bestandstype verkeerd staat ingesteld, maar daarover laten. De NullExtractor geeft alleen bij Extract() een default waarde terug:

public interface INullExtractor : IExtract
{
}

public class NullExtractor : ExtractorBase, INullExtractor
{
   public override string FullPath { set; get; }

   public override void GetNowPlayingData()
   {
   // Do nothing
   }

   public override ArtistTitleModel Extract()
   {
      return new ArtistTitleModel
      {
         Artist = NowPlayingConstants.DefaultArtist,
         Title = NowPlayingConstants.DefaultTitle
      };
   }
}

File lock

Het probleem waar ik tegenaan liep was dat een bestand soms nog gelocked kan zijn: de automatisering had het bestand nog niet vrij gegeven. Hiervoor vond ik op internet een oplossing. Omdat deze voor alle drie de implementaties van toepassing is, heb ik een abstract “ExtractorBase” geïntroduceerd.
Hierin zet ik een method “GetLockedFileContent” die de inhoud als array teruggeeft en die door alle implementaties gebruik kan worden:

public abstract class ExtractorBase : IExtract
{
   protected string[] GetLockedFileContent(string filename)
   {
      var content = new List<string>();
      using (var logFileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
      {
         using (var logFileReader = new StreamReader(logFileStream))
         {
            while (!logFileReader.EndOfStream)
            {
               content.Add(logFileReader.ReadLine());
            }
         }
      }

      return content.ToArray();
   }

   public abstract string FullPath { get; set; }
   public abstract void GetNowPlayingData();
   public abstract ArtistTitleModel Extract();
}

Het sleutelwoord is hier “FileShare.ReadWrite” bij het lezen.

De DaletSender versie heeft de artiest in regel 1 en de titel in regel 2, da’s met een array geen rocket science:

public interface IDaletSenderExtractor : IExtract
{
}

public class DaletSenderExtractor : ExtractorBase, IDaletSenderExtractor
{
   private string[] fileContent;

   public override string FullPath { set; get; }

   public override void GetNowPlayingData()
   {
      fileContent = GetLockedFileContent(FullPath);
   }

   public override ArtistTitleModel Extract()
   {
      var artistTitleModel = new ArtistTitleModel
      {
         Artist=fileContent[0], 
         Title= fileContent[1]
      };

      return artistTitleModel;
   }
}

De OrbanPadSerialAsciiExtractor implementatie is nagenoeg identiek, op de Extract na:

var delimiter = Convert.ToChar(OrbanPADSerialDelimiter);
var artistTitleArray = fileContent[0].Split(delimiter);
 
var artistTitleModel = new ArtistTitleModel
{
   Artist = artistTitleArray[0].Split(KeyValueDelimeter)[1],
   Title = artistTitleArray[1].Split(KeyValueDelimeter)[1]
};

De DaletXml-implementatie is wat complexer. Ik kan het bestand met GetLockedFileContent lezen, maar wil de XML meteen als Model terug hebben. Dit kan met de XmlSerializer van dotNet, hiervoor heb ik al een Worker van een ander project. Deze zou ook last hebben van de filelock, dus heb ik deze met een kleine aanpassing als XmlFileSerializeWorker geïmplementeerd.

Tevens heb ik een “BroadcastMonitor“-class gemaakt met de structuur van de XML gemaakt. De worker maakt gebruik van generics, bij het creëren geef ik de “BroadcastMonitor“-class mee: de XML moet altijd dezelfde structuur hebben en kan daarom met behulp van de XmlSerializer altijd naar dit model worden omgezet.

public class DaletXmlExtractor : ExtractorBase, IDaletXmlExtractor
{
   private IXmlFileSerializer<BroadcastMonitor> xmlFileSerializer;
   private BroadcastMonitor xmlContent;

   public DaletXmlExtractor()
   {
      xmlFileSerializer = new XmlFileSerializeWorker<BroadcastMonitor>();
   }

   public override string FullPath { set; get; }

   public override void GetNowPlayingData()
   {
      xmlContent = xmlFileSerializer.Read(FullPath);
   }

   public override ArtistTitleModel Extract()
   {
      var artistTitleModel = new ArtistTitleModel 
      { 
         Artist = xmlContent.Current.artistName, 
         Title = xmlContent.Current.titleName 
      };

      return artistTitleModel;
   }
}

IProcess

Het werk zelf wordt bepaald door de NowPlayingProcessService. Deze baseer ik op mijn “Proces-Design-Pattern, deze zal vast in veel betere versies bestaan maar zo heb ik hem bedacht:

public interface IProcess
{
   void Prepare();
   void Execute();
   void HandleResult();
}

Bij bijna alle processen is het werk op deze manier in te delen:

  1. Prepare: haal de gegevens op, en zorg voor de nodige instellingen.
  2. Execute: doe wat er gedaan moet worden.
  3. HandleResult: verwerk het resultaat.

In dit geval volgt het grotendeels de stappen van mijn IExtract -interface. Want het process voert alleen maar uit; wat er gedaan wordt bepaald de gekozen implementatie. Deze implementatie wordt elders geselecteerd en aan de constructor meegegeven, (Dependency injection). In INowPlayingProcessService voeg ik alleen nog het veld FullPath toe: een doorgeefluik voor de locatie van het nowplaying-bronbestand.

public interface INowPlayingProcessService : IProcess
{
   string FullPath { set; }
}

De class zelf begint alsvolgt:

public class NowPlayingProcessService : INowPlayingProcessService
{
   private readonly IExtract extractor;

   public NowPlayingProcessService(IExtract extractor)
   {
      this.extractor = extractor;
   }

   public string FullPath
   {
      set { extractor.FullPath = value; }
   }
 
   public void Prepare()
   {
      extractor.GetNowPlayingData();
   }
/* de rest van de class*/

Vanuit de class die NowPlayingProcessService gebruik, moet dus de property “FullPath” worden meegegeven, waarna de Prepare method kan worden uitgevoerd. Deze lees het bestand en slaat de gegevens in een private veld in de extractor op.

Bij Execute krijg ik de uiteindelijke string met de artiest en titel terug. Om deze in de laatste stap (HandleResult) te bruiken moet deze waarde wel ergens in de NowPlayingProcessService bewaren:

private string fileText;

Nu kan ik de Execute ook aanroepen:

public void Execute()
{
   var artistTitleModel = extractor.Extract();
   fileText = string.Format("{0} - {1}", 
                 artistTitleModel.Artist, 
                 artistTitleModel.Title);
}

Bij de HandleResult wil ik het resultaat in een bestandje op de lokatie “TargetFilePath” wegschrijven:

public void HandleResult()
{
   File.WriteAllText(TargetFilePath, fileText);
}

Het grote voordeel is dus dat NowPlayingProcessService-class niet hoeft te weten waar de data vandaan komt, of hoe dat om te zetten. Zijn taak is alleen het werkprocess uit te voeren.

Factory pattern

Waar wordt dan bepaald welke implementatie gebruikt wordt? Dat wordt afgehandeld bij de start van de NowPlayingFileWatcher. Deze volgt de keuze die wordt gemaakt in een configuratiesetting of een parameter die een gebruiker kan meegeven als de applicatie wordt gestart. Dat deel heb ik hier weggelaten, maar het grote voordeel is dat je dan niet iedere keer je code moet aan passen en opnieuw builden.

Hiervoor wordt het Factory Pattern gebruikt in de constructor van de NowPlayingFileWatcher:

private readonly INowPlayingProcessService nowPlayingProcessService;
 
public NowPlayingFileWatcher()
{
   IExtractFactory extractFactory = new ExtractFactory();
   IExtract extractor = extractFactory.Get(selectedFileFormat);

   nowPlayingProcessService = new NowPlayingProcessService(extractor);
}

Alle implementaties zijn gebaseerd op dezelfde interface IExtract. De ExtractFactory is een class die alleen verantwoordelijk is om de juiste implementatie van IExtract te selecteren op basis van de meegegeven keuze. Deze wordt vervolgens met Depency Injection meegegeven bij het creëren van de NowPlayingProcessService.

Change event

Het enige wat nu nog ontbreekt is de code voor de fw_Changed:

private void fw_Changed(object sender, FileSystemEventArgs e)
{
   if (e.ChangeType != WatcherChangeTypes.Changed) return;

   try
   {
      fw.EnableRaisingEvents = false;

      nowPlayingProcessService.FullPath = e.FullPath;
      nowPlayingProcessService.Prepare();
      nowPlayingProcessService.Execute();
      nowPlayingProcessService.HandleResult();
   }
   finally
   {
      fw.EnableRaisingEvents = true;
   }
}

Hierin worden alleen de stappen aangeroepen van nowPlayingProcessService. Op deze plek hoeft niemand zich druk te maken welke implementatie er is gekozen, het path naar het bestand wordt ingesteld en verder is het Prepare, Execute en HandleResult.
Je ziet nog dat de fw.EnableRaisingEvents wordt uitgezet en aan het eind weer wordt ingeschakeld: als het process wordt uitgevoerd, negeren we eventuele nieuwe aanroepen als het bronbestand veranderd.

Wat een toestand!

Het lijkt veel code voor iets simpels, maar de kracht is dat ik nu heel snel een nieuwe implementatie kan toevoegen. Ik moet me aan het ‘contract’ houden van de IExtract interface voor de implementatie, en de keuze toevoegen aan de factory.

Dat is Clean Coding (zo veel mogelijk dan): je code moet leesbaar zijn, je moet niet verdwalen in een helse onleesbare if-then-else boom. Da’s fijn voor jezelf (ja echt!) als je over een jaar je code terugziet of voor je collega, als jij daar niet meer rond loopt.

Tot slot: het is work in progress, er zijn nog wel een paar onderdelen te verbeteren, bijv. validatie om uitzonderlijke situaties af te vangen.

Ik heb nog drie dingen op mijn lijst:

  • de applicatie met Topshelf als service te kunnen laten draaien
  • met een Window-forms (of beter als WPF applicatie) kunnen configureren
  • logging toevoegen

Now play!

Advertisements

One thought on “Somebody’s watching me nowplaying

  1. Pingback: Somebody’s watching me nowplaying (2) | Radioavontuur

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s