1

First of all, I'm sorry for my English.

So this started as a school project but since I was learning some much I decided to continue with it. My goal was to make a music player for Windows and I started really simple, just by getting all the files from a given directory and list them in the main window of my application. But soon that escalated and I started using MVVM, SQL Server along with EF6, which I had to learn by myself so, obviously there are some things I don't fully understand. Then after I made a version that would run and be kinda nice to use, I thought of implementing a 'new feature': a File Watcher. Every time something changes in the specific folder I get an event and I have to do something with it.

For the database related code, I used the Repository Pattern and Unit of Work, and even though I had to do a lot of research, trial and error, I'm pretty sure this part of my application is working fine.


The problem comes with the File Watcher. I'll try to be as concise as possible.

So here is the SystemWatcher class:

public class SystemWatcher
{
    private Handler _handler;
    private FileSystemWatcher _fileSystemWatcher;
    private UnitOfWork _unitOfWork = new UnitOfWork();
    private MetadataParser _metadataParser = new MetadataParser();

    public SystemWatcher(string path, string fileType)
    {

        _fileSystemWatcher = new FileSystemWatcher(path, fileType);
        _handler = new Handler(_unitOfWork, _metadataParser, path);

        _fileSystemWatcher.Path = path;
        _fileSystemWatcher.Filter = fileType;
        _fileSystemWatcher.EnableRaisingEvents = true;
        _fileSystemWatcher.Created += FileChanged;
        _fileSystemWatcher.Changed += FileChanged;
        _fileSystemWatcher.Deleted += FileChanged;
        _fileSystemWatcher.Renamed += FileRenamed;  
        
    }

    public void FileChanged(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine("{0} ----- {1}", e.FullPath, e.ChangeType);
        _handler.FileChangedHandler(e);
    }

    public void FileRenamed(object sender, RenamedEventArgs e)
    {
        Console.WriteLine("{0} ----- {1}", e.FullPath, e.ChangeType);
        _handler.FileRenamedHandler(e);
    }
}

Which in a test console application I instantiate like this:

SystemWatcher systemWatcher = new SystemWatcher("C:\\Users\\ricar\\Music\\Música", "*.mp3");

My thought on creating a separated handler class was to abstract a little bit the code and to not have too much code inside the System Watcher class.

This is the Handler class:

public class Handler
{
    private UnitOfWork _unitOfWork;
    private MetadataParser _metadataParser;

    private string _path;

    public Handler(UnitOfWork unitOfWork, MetadataParser metadataParser, string path)
    {
        this._unitOfWork = unitOfWork;
        this._metadataParser = metadataParser;

        this._path = path;
    }


    public void FileChangedHandler(FileSystemEventArgs e)
    {
        if(e.ChangeType == WatcherChangeTypes.Deleted)
        {
            int affectedRecordId = this._unitOfWork.SongRepository.GetAll().First(_song => _song.musicCompletePath == e.FullPath).id;
            this._unitOfWork.SongRepository.Delete(affectedRecordId);
        }
        else if(e.ChangeType == WatcherChangeTypes.Created)
        {
            //My idea here is to have a list of tasks, add a task when a file has been created/inserted, and then loop through this list and run each task
            song newSongToDB = this._metadataParser.GetSongObjectAsync(e.FullPath, _path).Result;
            this._unitOfWork.SongRepository.Insert(newSongToDB);
        }
        this._unitOfWork.Save();
    }

    public void FileRenamedHandler(RenamedEventArgs e)
    {
        //TODO Encapsulate inside a try/catch
        //Gets the ID of the renamed file to use in the Update() function
        int affectedRecordId = this._unitOfWork.SongRepository.GetAll().First(_song => _song.musicCompletePath == e.OldFullPath).id; 
        //Gets the object that corresponds to the id
        song affectedRecord = this._unitOfWork.SongRepository.GetByID(affectedRecordId);

        //Updates the path and the file name
        affectedRecord.musicCompletePath = e.FullPath;
        affectedRecord.musicFilename = e.Name;

        //Creates a new object, for the sake of comprehension
        song newDBRecord = affectedRecord;

        this._unitOfWork.SongRepository.Update(affectedRecordId, newDBRecord);
        this._unitOfWork.Save();
    }


}

This is the MetadataParser class

public class MetadataParser
{
    int i = 0;
    public async Task<song> GetSongObjectAsync(string targetSongPath, string path)
    {
        return await Task.Run(() => {
            song newSong = new song()
            {
                title = !string.IsNullOrEmpty(TagLib.File.Create(targetSongPath).Tag.Title) ?
                        TagLib.File.Create(targetSongPath).Tag.Title :
                        targetSongPath.Substring(path.Length + 1),
                artist = !string.IsNullOrEmpty(TagLib.File.Create(targetSongPath).Tag.FirstPerformer) ?
                            TagLib.File.Create(targetSongPath).Tag.FirstPerformer :
                            !string.IsNullOrEmpty(TagLib.File.Create(targetSongPath).Tag.FirstAlbumArtist) ?
                            TagLib.File.Create(targetSongPath).Tag.FirstAlbumArtist :
                            "Desconhecido",
                fileIndex = i++,
                sourceDirectory = path,
                musicCompletePath = targetSongPath,
                albumName = TagLib.File.Create(targetSongPath).Tag.Album,
                musicFilename = targetSongPath.Remove(0, path.Length + 1)
            };
            return newSong;
        });
    }
}

And finally, this is the UnitOfWork class

public class UnitOfWork : IDisposable
{

    private HubohDBEntities _context = new HubohDBEntities();
    private GenericRepository<song> _songRepository;
    private GenericRepository<playlist> _playlistRepository;


    public GenericRepository<song> SongRepository
    {
        get
        {
            if(this._songRepository == null)
            {
                this._songRepository = new GenericRepository<song>(_context);
            }
            return _songRepository;
        }
    }

    public GenericRepository<playlist> PlaylistRepository
    {
        get
        {
            if (this._playlistRepository == null)
            {
                this._playlistRepository = new GenericRepository<playlist>(_context);
            }
            return _playlistRepository;
        }
    }

    public bool Save()
    {
        try
        {
            _context.SaveChanges();
            return true;
        }
        catch
        {
            return false;
        }        
    }



    #region IDisposable Support
    private bool disposed = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
            this.disposed = true;
        }
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}

So you can see that the MetadataPaser class has a method which is an async Task that returns the song object created with the given file path (first argument) and directory path (second argument). Initially, this method wasn't an async Task, just a simple method returning an object of type song that was then used to insert onto the database. This worked out just fine in the beginning but then I decided to move 200 files to this folder just to see what happens. And guess what? ...

No, you have to guess it!!

Kidding. Some objects were inserted while others weren't. So I guess that the Windows file move/create/delete etc is too fast for the application to extract the metadata from the mp3 files and insert them onto the database.

My solution, and here is where I need your help, is to create a List<Task<song>> and every time the event is fired insert the correspondent task to that list, and once that List<Task<song>> has a Count() bigger than 0 starts executing all the task inside it.

  1. So how do I implement it?
  2. Should I use some Design Pattern with the SystemWatcher thing in my application?
  3. Should I store the files path that have been moved onto a .txt file and then using a timer, every 2 minutes check if the .txt file has something and insert it onto the database?
  4. Should I change something in the code I posted?

I hope I was succinct enough that someone could understand and help. Thanks in advance :)

  • Can you add code for your `UnitOfWork`? I'm assuming it has one instance of context, so I would start from not sharing `UnitOfWork` between different handlers and creating a new one on any new event inside the handler. – Guru Stron Oct 18 '20 at 09:05
  • Hi, I've added the code for my `UnitOfWork` class. I see what you are saying but creating a new instance for the `UnitOfWork` for 200 (+/-) events, corresponding to 200 (+/-) files, would make any difference? @GuruStron – Ricardo Rodrigues Oct 18 '20 at 10:35
  • 2
    I was, once again, searching on the internet, and I came across this comment on StackOverflow; [link](https://stackoverflow.com/questions/6943908/using-filesystemwatcher-with-multiple-files): "Also, set buffer size to larger than default to avoid buffer overflow. It happens when more than 25 files are dropped in source directory (in my test). If 200 files dropped, event handler is only called for few files, not all. _watcher.InternalBufferSize = 65536; //Max size of buffer" – Ricardo Rodrigues Oct 18 '20 at 10:43

0 Answers0