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.
- So how do I implement it?
- Should I use some Design Pattern with the
SystemWatcher
thing in my application? - 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?
- Should I change something in the code I posted?
I hope I was succinct enough that someone could understand and help. Thanks in advance :)