1
\$\begingroup\$

Simple Playlist Modifier

This program is a search and replace program for text based files. The main goal of the program is to replace directory paths to music files.

The idea is to have the program read files from input and then validate the given parameters (a file path in this case).

The Source M3U Contains -- created by VLC

#EXTM3U
#EXTINF:330,Martin Solveig, Dragonette & Idoling!!! - Big in Japan (Les Bros Remix)
C:\User\Name\Music\Singles & Misc\House\02 Big in Japan (Les Bros Remix).mp3
... ... and many, many, more songs.

I wanted all of the files to read as below without altering each one painstakingly in File Explorer, Finder, or Nautilus.

The Modified M3U Contents -- modified with plist

#EXTM3U
#EXTINF:330,Martin Solveig, Dragonette & Idoling!!! - Big in Japan (Les Bros Remix)
D:\Music\Singles & Misc\House\02 Big in Japan (Les Bros Remix).mp3
... ... and many, many, more songs. you get the point.

I needed a way to edit large playlists with ease and speed. So I made this program to help manage that.

There are 3 distinct Arguments and each is an attribute of its base option: the source file, source file path, and target file path.

As an example:

plist [file name] [source file path pattern] [target file path pattern]

plist mymusic.m3u c:\users\username\Music d:\Music

Think of it like strstr() with the haystack and needle, only I'm searching literally strand by strand for the needles I'm looking for and doing it for all occurrences, not just the first occurrence found.

This program is built with VLC M3U playlists in mind and no other type.

The program does produce a backup file before processing and making alterations. If all else fails, just delete the file extension .backup so that it reads .m3u only.

The backup files are truncated with each use, but does generate an identifier based on the original files name. So be cautious.

I used MinGW to compile it.

Compiled as: $ gcc -std=c99 -Wall -Wextra -Wpedantic convert_plist.c -o plist

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

#ifdef BUFSIZ
    #undef BUFSIZ
    #define BUFSIZ 1000 
#endif

// if more symbols should be addressed, adjust as neccessary.
// for example, use an array if multiple characters are to be 
// utilized as tokens to ignore or tokens to acknowledge...
const int BAD_INPUT = -1; //return code for bad input
const int BAD_BAKUP = -2; //--- --- bad backup file
const int BAD_BFMOD = -3; //--- --- bad backup file modification
const int IGN_ITEMS = 1; //max number of ignored tokens
const int ACK_ITEMS = 6; //max number of acknowledged tokens
const int EXT_ITEMS = 4; //max number of file extensions

typedef _Bool bool; //typedef to prettify 

//define bool keywords true and false
const bool true = 1;
const bool false = 0;

//use lables to differentiate argument types
enum ARGUMENT { self = 1, source, target, met };

const char * m3u = ".m3u";

void rmnewline(char * expression)
{//remove the newline character
        char * newline = NULL;
        size_t position, copylen;
        size_t stringlen = strlen(expression);

        if (NULL != (newline = strrchr(expression, '\n')))
        {//remove the newline if expression is valid
            copylen = strlen(newline);
            position = stringlen - copylen;
            expression[position] = '\0';
        }   
}

bool valid_args(int argc)
{//validate the number of correct arguments
    bool status = false;

    switch(argc)
    {//assume at least one argument is missing, else all arguments are met (since 4 are valid)
        case self: 
            puts("\nplist - Portable Playlist Modifier\n"
                 "This program replaces the Source File Path Pattern with the Target Path Pattern.\n\n"
                 "Usage: plist [file_name] [source_path_pattern] [target_path_pattern]\n\n"
                 "[file_name] is the playlist file to process.\n"
                 "[source_path_pattern] is the string pattern to be replaced.\n"
                 "[target_path_pattern] is the string pattern that replaces the source pattern.\n\n"
                 "Example: plist /path/to/my/playlist.m3u /current/path/to/my/music /new/path/to/my/music\n"
                 "Use M3U playlist files, or modify at your own expense to manage other playlist types.");
            break;
        case source:
            puts("No [source_path_pattern] was provided.");
            puts("Invoke [plist] without arguments for help.");
            break;
        case target:
            puts("No [target_path_pattern] was provided.");
            puts("Invoke [plist] without arguments for help.");
            break;
        case met:
            status = true;
            break;
        default:
            puts("Invalid use of [plist].");
            puts("Invoke [plist] without arguments for help.");
            break;
    }

    return status;
}

bool valid_m3u_ext(const char * filename)
{//validate the m3u file extension exclusively
    bool status = false;

    char * container = strrchr(filename, '.');

    (!strcmp(container, m3u)) ? status = true : puts("Invalid argument(s) supplied.");

    return status;
}

char * mod_filename(const char * filename)
{//modify the file name so that it contains a ".backup" extension
    size_t nullchar = 1;

    char * temp = malloc(strlen(filename) + strlen(".backup") + nullchar);

    assert(NULL != temp);

    strcpy(temp, filename);

    rmnewline(temp); //if it does not find a newline, it does nothing

    strcat(temp, ".backup");

    return temp;
}

void copy_playlist(const char * filename, FILE * playlist, FILE * backup)
{//copy the file contents and report the number of bytes counted
    int ch, count = 0;

    while ((ch = getc(playlist)) != EOF)
    {
        putc(ch, backup);
        ++count;
    }

    printf("Backup created successfully: %s\n", filename); 
    printf("%d bytes were copied there.\n\n", count);
}

char * backup_playlist(char * filename)
{//backup the playlist file
    char * temp = mod_filename(filename);

    //open playlist file for read-only
    FILE * playlist = fopen(filename, "r");
    //create a new file for writing and updating
    FILE * backup = fopen(temp, "w");

    if (NULL == playlist || NULL == backup)
    {
        puts("Failed to open and backup file(s).");
        return NULL;
    }

    copy_playlist(temp, playlist, backup);

    //close the opened files
    fclose(playlist);
    fclose(backup);

    return temp;
}

void copy_metadata(FILE * newplist, FILE * oldplist)
{
    int ch;

    putc('#', newplist); //make sure you add the newline!

    while ('\n' != (ch = getc(oldplist))) 
    {//if the line starts with a '#', copy it over
        putc(ch, newplist);
    }

    putc('\n', newplist);
}

void compare_src_txt(char * source, FILE * oldplist)
{//compare source text and if it matches, skip over it
    int ch;

    for (size_t i = 1; i < strlen(source); i++)
    {
        if ((ch = getc(oldplist)) != source[i]) 
        {
            break;
        }
    }
}

void write_target_txt(char * target, FILE * newplist)
{//write the target text to where the source text was
    for (size_t i = 0; i < strlen(target); i++)
    {
        putc(target[i], newplist);
    }
}

void cat_src_txt(FILE * newplist, FILE * oldplist)
{//concatenate the rest of the source text to the file
    int ch;

    while ('\n' != (ch = getc(oldplist)))
    {//copy over the rest of the source text
        putc(ch, newplist);
    }

    putc('\n', newplist); //make sure you add the newline!
}

bool char_filter(char * source, char * target, FILE * newplist, FILE * oldplist)
{//read file character by character -- tedious, i know, but precise.
    int ch;

    while (EOF != (ch = getc(oldplist)))
    {
        switch(ch)
        {
            case '#':
                copy_metadata(newplist, oldplist);
                break;
            case '/': //"root" for unix
            case 'c': //"c:\" for windows
            case 'C':
            case 'd': //"d:\" for windows
            case 'D':   
                compare_src_txt(source, oldplist);
                write_target_txt(target, newplist);
                cat_src_txt(newplist, oldplist);
                break;
            default:
                return false;
        }
    }

    return true;
}

bool modify_playlist(char * filename, char * backup, char * source, char * target)
{//modify the playlist using the given source, and target, text
    //open playlist file for write-only
    FILE * newplist = fopen(filename, "w");

    //use the backup for read-only
    FILE * oldplist = fopen(backup, "r");

    if (NULL == newplist || NULL == oldplist)
    {//make sure the files were opened
        puts("Failed to open and backup file(s).");
        return false;
    }

    if (!char_filter(source, target, newplist, oldplist))
    {
        puts("Invalid initializing of a character after newline.");
        return false;
    }

    fclose(newplist);
    fclose(oldplist);

    printf("%s was modified successfully!\n", filename);

    return true;
}

int main(int argc, char * argv[])
{
    if (!valid_args(argc))
    {//must have at least one argument
        return BAD_INPUT;
    }

    if (!valid_m3u_ext(argv[1]))
    {//must be a defined file extension type
        return BAD_INPUT;
    }

    //each pointer now has a given name instead of argv[SOME_VALUE]
    char * filename = argv[1];
    char * source = argv[2];
    char * target = argv[3];
    char * backup = backup_playlist(filename);

    if (NULL == backup)
    {//backup must be good before moving on to writing data
        return BAD_BAKUP;
    }

    if (!modify_playlist(filename, backup, source, target))
    {
        return BAD_BFMOD;
    }

    return 0;
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Simplify rmnewline

Currently, rmnewline() does some complicated math to remove the last newline character:

void rmnewline(char * expression)
{//remove the newline character
        char * newline = NULL;
        size_t position, copylen;
        size_t stringlen = strlen(expression);

        if (NULL != (newline = strrchr(expression, '\n')))
        {//remove the newline if expression is valid
            copylen = strlen(newline);
            position = stringlen - copylen;
            expression[position] = '\0';
        }   
}

All the math is simply figuring out where the newline character is, which is being pointed to by newline. So all you really have to do is:

void rmnewline(char * expression)
{//remove the newline character
    char * newline = strrchr(expression, '\n');
    if (NULL != newline)
        *newline = '\0';
}

Copying a file

Your copy_playlist() function copies the playlist character by character, returning the size of the file. You could do better by:

  1. Using fseek() and ftell() to measure the size of the file.
  2. Using fread() and fwrite() to copy the file in blocks (or even in one big block) instead of character by character.

The filter

Although the search and replace filter works if all the files belong to the same path, it doesn't make sure that the source path matches the expected path. It just skips over any part of the source path that matches the expected path. You could do better by reading one whole line from the source file, comparing its prefix with the expected source path, and then replacing the prefix if it matches. If it doesn't match, you can just output the line unchanged.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.