DEV Community

Cover image for How Software Engineering Skills Can Improve Your Everyday Life.
Serhii Korol
Serhii Korol

Posted on

How Software Engineering Skills Can Improve Your Everyday Life.

A few days ago, I discovered a music collection in my archive that already had a playlist. However, when I opened it, I noticed that the track tags were incorrect. At first, this didn’t seem like a major issue—after all, track tags can be edited manually. But there was a big problem: the playlist contained 500 tracks with incorrect tags. Since all the tracks were labeled under "Various Artists," they were stored in a single folder, even though the collection actually included many different artists.

incorrect playlist

Manually fixing each track would be extremely time-consuming, so I decided to create an application to automate the process.

The application consists of three main components:

  1. Generating a new playlist with corrected file paths and names.
  2. Organizing files into separate folders based on the artist.
  3. Updating track tags with the correct information.

Core application

I decided to use three input parameters: the path to the incorrect playlist, the path for the corrected output playlist, and the source path for the music tracks. The input playlist, which consists only of file names, looks like this:

broken playlist

If you open this playlist file in the Music app, it will convert the playlist into its own format and copy the music files. However, the tracks will still have incorrect metadata and will all be placed in a single folder.

converted playlist

Converting the playlist or copying files manually isn’t necessary. However, the playlist file still needs to be modified to correct the file names. The Music app can handle file copying, but this requires saving the corrected files to a separate output folder, from which the app will copy them to its own library. After that, you'd need to manually clean up the extra files. To streamline the process, I decided to automate everything myself.

internal static class Program
{
    private static void Main(string?[] args)
    {
        string? inputPath = null;
        string? outputPath = null;
        string? musicSourcePath = null;

        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == "--input" && i + 1 < args.Length)
                inputPath = args[++i];
            else if (args[i] == "--output" && i + 1 < args.Length)
                outputPath = args[++i];
            else if (args[i] == "--source" && i + 1 < args.Length)
                musicSourcePath = args[++i];
        }

        if (string.IsNullOrEmpty(inputPath) || string.IsNullOrEmpty(outputPath) || string.IsNullOrEmpty(musicSourcePath))
        {
            Console.WriteLine(
                "Usage: app --input <inputPlaylistPath> --output <outputPlaylistPath> --source <musicSourcePath>");
            return;
        }

        string[] lines = File.ReadAllLines(inputPath);

        using (StreamWriter writer = new StreamWriter(outputPath))
        {
            writer.WriteLine("#EXTM3U");

            foreach (string line in lines)
            {
                if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
                {
                    writer.WriteLine(line);
                    continue;
                }

                string appleMusicFilePath = DetectAppleMusicFolder();

                (string fixedExtinf, string fixedPath, string artist, string title, string album) =
                    FixEntry(line, musicSourcePath, appleMusicFilePath);

                writer.WriteLine(fixedExtinf);
                writer.WriteLine(fixedPath);

                CopyAndUpdateMetadata(line, fixedPath, musicSourcePath, artist, title, album);
            }
        }

        Console.WriteLine("Playlist fixed and saved to: " + outputPath);
    }
}
Enter fullscreen mode Exit fullscreen mode

Let me walk you through the code. First, we check the input arguments and retrieve the Music app folder. Look for the line:

appleMusicFilePath = DetectAppleMusicFolder();
Enter fullscreen mode Exit fullscreen mode

Typically, the Music app stores user files in a different location than the Apple Music library. We need to verify this folder’s existence. If it doesn’t exist, we either create it or return its path.

    static string DetectAppleMusicFolder()
    {
        string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        string potentialPath = Path.Combine(homePath, "Music", "iTunes", "iTunes Media", "Music");
        if (!Directory.Exists(potentialPath))
        {
            Directory.CreateDirectory(potentialPath);
        }

        return potentialPath;
    }
Enter fullscreen mode Exit fullscreen mode

Creating a New Playlist

Now, locate the following line:

(string fixedExtinf, string fixedPath, string artist, string title, string album) = FixEntry(line, musicSourcePath, appleMusicFilePath);
Enter fullscreen mode Exit fullscreen mode

This is where we generate the output playlist. Each line is processed to extract the track number, artist name, and song title. However, the album title is missing because the playlist file doesn’t include this information.

static (string, string, string, string, string) FixEntry(string line, string? musicSourcePath,
        string appleMusicPath)
    {
        string pattern = @"(\d+) - (.+?) - (.+?)\.mp3";
        Match match = Regex.Match(line, pattern);

        if (!match.Success) return (line, line, "", "", "");
        string trackNumber = match.Groups[1].Value;
        string artist = match.Groups[2].Value.Trim();
        string title = match.Groups[3].Value.Trim();

        if (musicSourcePath == null) return (line, line, "", "", "");
        string sourceFilePath = Path.Combine(musicSourcePath, line);

        string album = GetAlbumFromMetadata(Path.GetFileName(sourceFilePath), musicSourcePath);


        string directory = Path.Combine(appleMusicPath, artist, album);
        if (!Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        string filename = $"{title}.mp3";
        string newPath = Path.Combine(directory, filename);

        string newExtinf = $"#EXTINF:0,{title} - {artist}";

        return (newExtinf, newPath, artist, title, album);
    }
Enter fullscreen mode Exit fullscreen mode

If you look at the following line:

string album = GetAlbumFromMetadata(Path.GetFileName(sourceFilePath), musicSourcePath);
Enter fullscreen mode Exit fullscreen mode

You'll find the method responsible for retrieving the album title from the metadata.

static string GetAlbumFromMetadata(string originalFilename, string musicSourcePath)
    {
        try
        {
            string originalFilePath = Path.Combine(musicSourcePath, originalFilename);

            if (File.Exists(originalFilePath))
            {
                var file = TagLib.File.Create(originalFilePath);
                return file.Tag.Album ?? "Unknown Album";
            }

            Console.WriteLine($"File not found for metadata extraction: {originalFilePath}");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error reading metadata: " + ex.Message);
        }

        return "Unknown Album";
    }
Enter fullscreen mode Exit fullscreen mode

For each file, we extract the album tag from its metadata. If the album tag is missing, we replace it with a placeholder.

Copying Files to Organized Folders

After generating the new playlist and correcting the file names, the next step is to copy the files to a temporary location. Locate the following line:

CopyAndUpdateMetadata(line, fixedPath, musicSourcePath, artist, title, album);
Enter fullscreen mode Exit fullscreen mode

This function handles copying the files and updating their metadata.

static void CopyAndUpdateMetadata(string originalFilename, string newPath, string? musicSourcePath, string artist,
        string title, string album)
    {
        try
        {
            if (musicSourcePath == null) return;
            string oldPath = Path.Combine(musicSourcePath, originalFilename);

            if (!File.Exists(oldPath))
            {
                Console.WriteLine($"Source file not found: {oldPath}");
                return;
            }

            string tempFile = Path.Combine(Path.GetDirectoryName(newPath) ?? string.Empty, Guid.NewGuid() + ".mp3");
            File.Copy(oldPath, tempFile, true);
            Console.WriteLine($"Created temp file: {tempFile}");

            File.SetAttributes(tempFile, FileAttributes.Normal);

            UpdateMetadata(tempFile, artist, title, album);

            if (File.Exists(newPath))
            {
                File.Delete(newPath);
            }

            File.Move(tempFile, newPath);
            Console.WriteLine($"Copied modified file: {tempFile} -> {newPath}");
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine($"Access denied: {ex.Message}");
            Console.WriteLine("Try running the program with elevated permissions (sudo).");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error copying/updating file: {ex.Message}");
        }
    }
Enter fullscreen mode Exit fullscreen mode

We copy the files with their new names to a temporary folder. Why use a temporary folder? This approach helps avoid modifying permissions on the user's directories, and the temporary folder is automatically deleted when it's no longer needed.

Updating Track Metadata

Now, we need to update the file metadata. Locate the following line:

UpdateMetadata(tempFile, artist, title, album);
Enter fullscreen mode Exit fullscreen mode

This function modifies the track's metadata with the correct artist, title, and album information.

static void UpdateMetadata(string filePath, string artist, string title, string album)
    {
        try
        {
            if (!File.Exists(filePath))
            {
                Console.WriteLine($"Error: Source file not found - {filePath}");
                return;
            }

            string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".mp3");

            if (File.Exists(tempFile))
            {
                File.Delete(tempFile);
            }

            File.Copy(filePath, tempFile, true);
            File.SetAttributes(tempFile, FileAttributes.Normal);

            if (!File.Exists(tempFile))
            {
                Console.WriteLine($"Error: Temp file creation failed - {tempFile}");
                return;
            }

            var file = TagLib.File.Create(tempFile);
            file.Tag.Performers = [artist];
            file.Tag.Title = title;
            file.Tag.Album = album;
            file.Save();

            File.Copy(tempFile, filePath, true);
            File.Delete(tempFile);

            Console.WriteLine($"Updated metadata: {title} - {artist} - {album}");
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine($"Access denied: {ex.Message}");
            Console.WriteLine("Try running the program with elevated permissions (sudo).");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error updating metadata: {ex.Message}");
        }
    }
Enter fullscreen mode Exit fullscreen mode

We copy the files again to prevent race conditions. This approach is simpler and more reliable than managing process synchronization manually.

Testing and Validation

Let's test the results. The corrected output playlist file now looks like this:

#EXTM3U
#EXTINF:0,Stairway To Heaven - Led Zeppelin
/Users/serhiikorol/Music/iTunes/iTunes Media/Music/Led Zeppelin/Bacobens Rock Top 500/Stairway To Heaven.mp3 
Enter fullscreen mode Exit fullscreen mode

When you open the playlist, you'll see that your music tracks now have the correct metadata.

new playlist

Final Thoughts

The code may not be perfectly optimized, but it was developed in just a few hours. This real-life example demonstrates how software engineering skills can help solve everyday problems efficiently.

I hope you found this article interesting. See you next time—happy coding!

As always, you can find the source code on my GitHub.

Buy Me A Beer

Top comments (0)