Project: WMCWeb. Draft design

The console application that I created and that is available for download has this unfortunate name mpconverter. In my opinion is not the best choice. The upcoming new version should be called something else that better describes my intentions. For now it will called WMCWeb.

We should be able to access UI through the browser. Application will be installed and run on the PC that does recording through Windows Media Center. We should be able to view a list of recorded files and pick the ones we want. Selected file will be added to Queue Manager and then processed for conversion. In addition application design needs to be flexible to incorporate new features like fetching metadata from other sources or using different methods. In addition, our primary choice of encoder is FFMPEG, but we should be able to swap it different encoder if we want to.

At this point it is much clearer what architectural choices need to be implemented to guarantee such flexibility. The project needs to be separated into distinctive layers starting with Domain that may have its own subdivisions. We should be careful over here because we really need to have division between various components, so we do not mix different objects and methods. I will explain what should be included in our Domain layer. For the Presentation layer we will use ASP.Net MVC and we will have Service Layer that sits in between Domain and Presentation. In the initial development we will have simple class and eventually we will move to WebApi. This way we will allow mobile clients to communicate with our application. The recordings data needs to be stored in database. I’m leaning towards using SQLite database in conjunction with Entity Framework 6. However, initially I may have to use Microsoft SQL Express or Local Database. The problem with SQLite is that it does not allow EF6 database migrations. I’m considering also Entity Framework Core that plays much nicer with SQLite. Regardless of that we will have include Repository pattern to separate ORM from Domain. This way we would be able to swap Entity Framework versions or pick completely different ORM like NHibernate.

Domain part is tricky. I found this to be the most challenging aspect of design. Defining required objects and their interactions was rather confusing. Some objects do not have their counterparts in real world and we don’t require any CRUD operations on our objects. UI would simply display information available to us and we will decide what to do with it.

I mentioned before that some sections of our Domain have their own functional responsibilities. The first section will be responsible for reading metadata from the recorded file. If you pick any file inside Windows Explorer, you would have information about this file like Date Created or Date Modified. This information is called Header and there are more than 250 different headers for each file. WTV file stores also Title and Description information. We need to pull this information from each file. We do not want iterate through each file and match headers that we require. We need to do it only once at the beginning of the scan and use their id numbers for each subsequent file. I used this approach in mpconverter before, but I came across interesting article that describes similar implementation on getting data from database using ordinals where you find id number based on column name. We just need the method that would do the same for us reading file using Microsoft Shell object. During scan process we will pull more than 20 headers. They all mixed, so we would have to do some reverse engineering and group them accordingly based on certain criteria. Entity Framework would take care of mapping each property to the right table. So far I came up with these required tables

 

FileInfo File TVSeries Movies Genres GrenresLink
FileId FileId FileId FileId GenreId FileId
Duration FileName SeriesName MovieTitle Name GenreId
DateCreated FilePath Description MovieYear
DateRecorded IsRerun Description
IsProtected Rating
ChannelNo
StationName
StationCallSign
FolderName
FolderPath

Before we write anything to database we need to have methods to identify new recordings, validate data and sanitize string to remove special invisible characters. During this initial step, we should also extract video frame from a file and store it locally as a thumbnail. We would use FFMPEG to do the job.

Code below is a rough idea of objects and their interactions. There is no actual method implementation, but it should give you some idea where this whole thing is heading. When I’m not home and I cannot use Visual Studio, I fire up browser and go to https://dotnetfiddle.net/ . This is an excellent tool for writing code and testing some ideas.


public class Program
{
      public static void Main()
     {

          var repo = new LocalFilesRepository("path");
          var reader = new ShellReader ();
          var parser = new ShellParser(reader);

          var repoService = new LocalRepositoryService (repo, parser);

          repoService.ScanFolder();
     }

}

public class LocalFilesRepository: ILocalRepository
{

    private string _path;
    private List<string> _files;
 
    public LocalFilesRepository (string path)

    {
           _path = path;
     }


    public IEnumerable<string> GetAllFiles()
    {
        return  new List<string> ();
    }
}

public class LocalRepositoryService
{
        private IEnumerable<string> _newFiles;
        private ILocalRepository _localRepository;

        private IParseMetadataFromFile _metadataFromFile;


        public LocalRepositoryService(ILocalRepository repo, IParseMetadataFromFile metadataFromFile)
        {
              _localRepository = repo;
              _metadataFromFile = metadataFromFile;
        }

       public void ScanFolder ()
       {
              this.GetNewFilesFromLocalRepository();
              this.GetFiles();
              this.SaveToDatabase();
       }

       private IEnumerable<string> GetNewFilesFromLocalRepository()
       {

            //from Enities
            HashSet<string> dbFiles = new HashSet<string> ();

            //_localRepository
           HashSet<string> localFiles = new HashSet<string> ();

           _newFiles = localFiles.Except(dbFiles);

           return _newFiles;
       }

       private IEnumerable<RecordingDto> GetFiles()
       {
            _metadataFromFile.PopulateOrdinals(_newFiles.GetFirstFile());

            return _metadataFromFile.PopulateRecordingDto(_newFiles);
       }


      private void SaveToDatabase()
      {
      }
}


public static class Extensions
{

       public static string GetFirstFile(this IEnumerable<string> files)
       {
            return "this string";
       }
}


public interface ILocalRepository
{
       IEnumerable<string> GetAllFiles();

}

public interface IParseMetadataFromFile
{
       void PopulateOrdinals(string file);
       IEnumerable<RecordingDto> PopulateRecordingDto(IEnumerable<string> files);
}


public interface IReadFile
{
       int GetOrdinal(string name);
       string GetName(int ord);
}

public class ShellParser: IParseMetadataFromFile
{
       private IReadFile _reader;

       public ShellParser(IReadFile reader)
       {
             _reader = reader;
       }


       public int OrdinalTitle {get; set;}
       public int OrdinalYear {get; set;}

      public void PopulateOrdinals(string file)
      {
           OrdinalTitle = _reader.GetOrdinal("some");
      }


     public IEnumerable<RecordingDto> PopulateRecordingDto(IEnumerable<string> files)
     {
           var name = _reader.GetName(1);
           var f = new List<RecordingDto> ();
           Console.WriteLine("I'm getting List of Dtos");
           return f;
     }
}


public class RecordingDto
{

}

public class ShellReader:IReadFile
{

     public int GetOrdinal(string name)
     {

         int ord = 1;
         Console.WriteLine("I'm getting ordinal number");
         return ord;
      }

     public string GetName(int ord)
     {
         string name = "ord";
         Console.WriteLine("I'm getting ordinal name");
         return name;
     }
}

Output:
I'm getting ordinal number
I'm getting ordinal name 
I'm getting List of Dtos

After everything is done and data is saved into our database we pull a list of recordings to be displayed in our UI. From there we pick the item that we are interested in and add it to our Queue. Technically speaking FFMPEG requires only input file path, output file path and arguments. But I think it would be more beneficial to pass to Queue Manager object that contains metadata and encoding profile. This QueueItem will have enough information for encoding job and renaming after it is completed. This way we would not have to go back to database to get necessary metadata. FFMPEG is little bit confusing. I do not think is necessary to create separate FFMPEG class with its own encoding method. It is already separate object that requires these three variables. However we need a method that would send our request to FFMPEG. I believe that extension method would be a right fit for this to work.


public class Program
{
       public static void Main()
       {
          var queueItem = new QueueItem();
          var queueManager = new QueueManager();

          queueManager.AddItemToQueueManager();

          var encoder = new Encoder();

          queueManager.StartEncoding(encoder);

       }
}

public class QueueItem
{
     public int QueueItemId {get; set;}
     public int MceRecordingId {get; set;}
     public int EncodingProfileId {get;set;}
     public MceRecording MceRecording {get;set;}
     public EncodingProfile EndcodingProfile {get;set;}
}

public class QueueManager
{
         private ICollection<QueueItem> _queueItems;
         private Queue<QueueItem> _queue;

         public QueueManager ()
         {
             _queueItems = new List<QueueItem>();
             _queue = new Queue<QueueItem>();
             //load _queueItems from Entity
         }

        //this method should use Recording GUID
        public void AddItemToQueueManager()
        {
             //save to Entity
             //addd to a list of item displayed by QueueManager

             var queueItem = new QueueItem();
            _queueItems.Add(queueItem);

        }

        public void StartEncoding (IEncode encoder)
        {

              foreach(QueueItem queueItem in _queueItems)
              {
                     encoder.StartEncoding(queueItem);
              }
        }
}

public interface IEncode
{
        void StartEncoding(QueueItem queueItem);
}

public class Encoder:IEncode
{
        public void StartEncoding(QueueItem queueItem)
        {
             queueItem.MoveFileToTemporaryFolder()
                      .ConvertToMpeg()
                      .ConvertToMkv()
                      .RenameFile();
        }
}

public static class Extensions
{
       public static QueueItem ConvertToMkv(this QueueItem queueItem)
       {
             Console.WriteLine("I convert file mkv/mp4");
             return queueItem;
       }

       public static QueueItem MoveFileToTemporaryFolder(this QueueItem queueItem)
       {
             Console.WriteLine("I move file to temporary folder");
             return queueItem;
        }

       public static QueueItem ConvertToMpeg(this QueueItem queueItem)
       {
              Console.WriteLine("I convert file to mpeg");
              return queueItem;
       }

       public static void RenameFile(this QueueItem queueItem)
       {
               Console.WriteLine("I rename file");
       }
}

public class MceRecording
{
        public int MceRecordingId {get; set;}
}

public class EncodingProfile
{
       public int EncodingProfileId {get; set;}
}

Output:
I move file to temporary folder
I convert file to mpeg
I convert file mkv/mp4 .
I rename file

I think that entire project is heading in the right direction. Once we establish this basic functionality of reading files, storing them into database, retrieving them and passing them to encoder, we can focus on additional functionality.

Leave a Reply

Your email address will not be published. Required fields are marked *