/*

 File:      videocat.c
 Author:    Graham Toal <gtoal@gtoal.com>
 Copyright: None.  You're welcome to use this any way you want.
 Created:   200808231514

 Revision history:

            200808231514 Initial version
            200809061157 Added some comments prior to release.
                         Removed unused procedure.
                         Made sure windows and unix sources were aligned.
*/          static char *version = "200809061157"; /*

 Description:

   This is a utility I hacked up to scan external drives for DVD images
   (and some other video files) in order to create a searchable database
   to help me find my videos.  (All my original DVDs are boxed up and
   stored.  Moving my DVD collection to HD has given us back about half
   a room of shelf space!)  If you only have one or two external drives,
   this won't help you much, but if you have a couple of dozen it'll 
   help a lot.  You also need some sort of database manager to actually
   use the data (unless you just want to grep it), but this outputs in
   CSV format so should be usable by most database that support an external
   load.

   Invoke with a drive letter as a parameter followed by a verbal description
   of the drive and its location, so you can find it later.  The code looks for
   VIDEO_TS.IFO (originally I checked using video_ts.vob but some valid dvd images
   didn't have one.  Later added some video files such as .avi and .divx),
   extracts the parent directory name, saves in database with the location
   of movie which you add as a command-line parameter.


   Example:  videocat d:\  "LaCie 500Gb marked 'box sets'" > lacie.csv  (on windows)
             videocat /mnt/videos  "Avi conversions from HD videocam"   (on unix)
             videocat j: >> catalog.csv

 TODO:
   extract drive title to store along with drive letter
   allow for updates to replace old data in csv file, if you carelessly merged
   all your runs of this program into one file.  Otherwise to recreate the
   main database you either have to work out how to remove selected entries
   or rerun the program on all external drives.
   find a windows programmer to put a gui over it and make it robust enough
   for use by non-programmers

 BUGS:
   Not 100% confident in the Windows tree-walking code.
   Not sure about portability across windows compilers.  I'm compiling
   it with TCC ( http://bellard.org/tcc/ ) and the code I based this
   on was previously compiled with LCC ( http://www.cs.virginia.edu/~lcc-win32/ )
   - anything else, you may need to tweak a little.
   There may be problems caused by specifying a trailing '\' in a directory name.
   Need to check on dos/windows that specifying "D:" does what is expected
   even if someone earlier did a "CD subdir" on that drive.  (Mind you, not
   entirely sure what *is* expected...)

----------------------------------------------------------
*/

/* Standard C libs */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>

#ifndef FALSE
#define FALSE (0!=0)
#define TRUE (0==0)
#endif

static char *progname;
static char *filename;
static int debug = FALSE, verbose = FALSE;

void DoOneFile(char *desc) {
  long long int block = 0LL;
  //FILE *videofile = fopen(filename, "rb");

  //if  (videofile == NULL) {
  //  if (errno != EACCES) {
  //    //fprintf(stderr, "%s: %s - %s\n", progname, filename, strerror(errno));
  //  }
  //  return; // may be locked etc.  Not fatal if cannot open every file...
  //          // (also, directories give 'permission denied')
  //}

  if (strcasecmp(filename+strlen(filename)-strlen("video_ts.ifo"), "video_ts.ifo") == 0) {
    char *s, Movie[1024];
    int l;
    l = strlen(filename);
    strcpy(Movie, filename);
    l = l - strlen("video_ts.ifo") - 1; Movie[l] = '\0';
    // There are some tweaks here to try to guess which part of the path is
    // the actual movie name.  It doesn't always get it right but hey, you have
    // the source code, make it do what you want...
    if (strcasecmp(Movie+l-strlen("video_ts"), "video_ts") == 0) {
      l = l - strlen("VIDEO_TS") - 1; Movie[l] = '\0';
    }
    s = strrchr(Movie, '\\');
    if (s == NULL) s = strrchr(Movie, '/');
    if (s == NULL) s = strrchr(Movie, ':');
    if (s == NULL) s = Movie; else {
      s += 1;
      if ((*s == 's' || *s == 'S') && (isdigit(s[1]))) {
        // ...\disk name\s1 d3\video_ts.ifo
        s = s - 1; *s = ' ';
        for (;;) {
          if (s == Movie) break;
          s = s - 1;
          if (*s == '/' || *s == '\\' || *s == ':') {
            s = s + 1;
            break;
          }
        }
      }
    }
    fprintf(stdout, "DVD,\"%s\",", s);
    fprintf(stdout, "\"%s\",", filename);
    fprintf(stdout, "\"%s\"", desc);
    fprintf(stdout, "\n");
  } else if (
             (strcasecmp(filename+strlen(filename)-strlen(".avi"), ".avi") == 0) ||
             (strcasecmp(filename+strlen(filename)-strlen(".divx"), ".divx") == 0) ||
             (strcasecmp(filename+strlen(filename)-strlen(".mpg"), ".mpg") == 0) ||
             (strcasecmp(filename+strlen(filename)-strlen(".mkv"), ".mkv") == 0) ||
             (strcasecmp(filename+strlen(filename)-strlen(".mp4"), ".mp4") == 0)
            ) {
    char *s, Movie[1024];
    int l;
    l = strlen(filename);
    strcpy(Movie, filename);
    s = strrchr(Movie, '.');
    fprintf(stdout, "%s,", s+1);
    *s = '\0';
    s = strrchr(Movie, '\\');
    if (s == NULL) s = strrchr(Movie, '/');
    if (s == NULL) s = strrchr(Movie, ':');
    if (s == NULL) s = Movie; else s += 1;
    fprintf(stdout, "\"%s\",", s);
    fprintf(stdout, "\"%s\",", filename);
    fprintf(stdout, "\"%s\"", desc);
    fprintf(stdout, "\n");
  }

  //fclose(videofile);
}

#ifdef __unix__
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>

int ForAllFiles(int level, const char *dirname, char *desc) {
  DIR *dd;
  struct dirent *dent;
  struct stat statbuf;
  char *fname;
  int scanret = 0;

    if ((dd = opendir(dirname)) != NULL) {
	while ((dent = readdir(dd))) {
#ifndef C_INTERIX
	    if (dent->d_ino)
#endif
	    {
		if (strcmp(dent->d_name, ".") && strcmp(dent->d_name, "..")) {
		    /* build the full name */
		    fname = calloc(strlen(dirname) + strlen(dent->d_name) + 2, sizeof(char));
		    sprintf(fname, "%s/%s", dirname, dent->d_name);

		    /* stat the file */
		    if (lstat(fname, &statbuf) != -1) {
			if (S_ISDIR(statbuf.st_mode) && !S_ISLNK(statbuf.st_mode)) {
			    ForAllFiles(level+1, fname, desc);
			} else {
			    // PERFORM ACTION: 
			    if(S_ISREG(statbuf.st_mode)) {
			      filename = fname;
			      DoOneFile(desc);
			    }
                        }
		    }
		    free(fname);
		}
	    }
	}
    } else {
	if (verbose) fprintf(stderr, "%s: Can't open directory %s\n", progname, dirname);
	return 53;
    }
    closedir(dd);
    return (scanret ? 1 : 0);
}
#else

#ifdef OLDSTYLE /* DOS API. Fails on some machines - infinite loop */

/* Hacked from "dir.c" at http://www-informatik.hs-harz.de/NVMCK/Labor_sapi.doc ... */

/* non-standard libs for findfirst etc under LCC compiler on Windows. Possibly others? */

#include <io.h>
#include <direct.h>

int ForAllFiles(int level, char *param, char *desc) {
  static char current[256];
  struct _finddata_t ft;
  long int lf;
  int bMore = FALSE;

  if (level == 0) {
    _getcwd(current, sizeof(current)-1);
    _chdir(param); // if dir as a param, do this
  }
  lf = _findfirst( "*.*", &ft);
  bMore = (lf != -1L);
  while (bMore) {
    if  ( ft.name[0] != '.')  {
      if  (ft.attrib & _A_SUBDIR) {
        _chdir(ft.name);
        ForAllFiles(level+1, param, desc);
        _chdir("..");
      } else {
        _getcwd(current, sizeof(current)-1);
        strcat(current, "\\");
        strcat(current, ft.name);
        filename = current; // another nasty global
        DoOneFile(desc); // BUT WOULD PREFER NOT TO IF IT IS A DIRECTORY!!!
                     // (relying on dir giving EACCES in WinXX)
      }
    }
    bMore = !_findnext(lf, &ft) ;
  }
  if (level == 0)_chdir(current);
  return 1;
}
#else /* WIN32 API */
#include <io.h>
#ifdef TRUE
#undef TRUE
#undef FALSE
#endif
#ifdef N
#undef N
#endif
#include <windows.h>

/*

  Program to walk the directory tree (from a nominated starting point)
  and print the names of all files found (excepting directories)


  Author:     Nick Gammon  <nick@gammon.com.au>
  Hacked by:  Graham Toal
  Web:        http://www.gammon.com.au

  Date:       8th August 1999
  Hacked on:  6th July 2006

  Usage:  treeinfo <starting_point>

  eg.     treeinfo c:\games

*/


void PrintWin32Error (DWORD ErrorCode)
{
 
  LPVOID lpMsgBuf;
  FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |  
                 FORMAT_MESSAGE_FROM_SYSTEM |     
                 FORMAT_MESSAGE_IGNORE_INSERTS,    
                 NULL,
                 ErrorCode,
                 MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                 (LPTSTR) &lpMsgBuf,    
                 0,    
                 NULL );
	fprintf(stderr, "%s", lpMsgBuf );
	LocalFree( lpMsgBuf );
}

void process_dir (char * dirname, 
                  char * subdirname, 
                  int level,
                  __int64 * totalsize,
                  long * totalfiles,
                  char *desc)
  {
  char * thisdir = NULL;
  WIN32_FIND_DATA fileinfo;
  HANDLE hdl;
  double mb;

  // we add 4 below for:
  //   null at end: 1
  //   \ between dir and subdir name : 1
  //   \* at end: 2

  thisdir = malloc (strlen (dirname) + strlen (subdirname) + 4);
  if (!thisdir)
    {
    fprintf(stderr, "*** Unable to allocate memory for directory name: %s\\%s\n", 
            dirname, subdirname);
    return;
    }

  strcpy (thisdir, dirname);
  if (strlen (subdirname) > 0)
    {
    strcat (thisdir, "\\");
    strcat (thisdir, subdirname);
    }
  strcat (thisdir, "\\*");

  if (strlen (thisdir) > _MAX_PATH)
    {
    fprintf(stderr, "*** pathname: \"%s\" is too long to process\n", thisdir);
    free (thisdir);
    return;
    }

  hdl = FindFirstFile (thisdir, &fileinfo);
  
  thisdir [strlen (thisdir) - 2] = 0;   // remove wildcard from end

  if (hdl == INVALID_HANDLE_VALUE)
    {
    DWORD nErr = GetLastError ();

    if (level != 0 && nErr == ERROR_NO_MORE_FILES)
      {
      free (thisdir);
      return;
      }

    //fprintf(stderr, "*** %s : ", thisdir);    
    //PrintWin32Error (nErr);
    free (thisdir);
    return;
    }

  do
    {
    
    if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      {
      if (strcmp (".", fileinfo.cFileName) != 0 &&
          strcmp ("..", fileinfo.cFileName) != 0)
        {
        __int64 total = 0;
        long    files = 0;
        process_dir (thisdir, fileinfo.cFileName, level + 1, &total, &files, desc);
        *totalsize += total;
        *totalfiles += files;
        }
      }
    else
      {
      LARGE_INTEGER nSize;
      char pathname[1024*10]; // beware of buffer overflow.  Need to fix this.

      nSize.HighPart = fileinfo.nFileSizeHigh;
      nSize.LowPart = fileinfo.nFileSizeLow;

      *totalsize += nSize.QuadPart;
      (*totalfiles)++;
      // if (level == 0) ...
      mb = (double) nSize.QuadPart / 1024.0 / 1024.0;
      // fprintf(stdout, "%s\\%s \n", /*mb,*/ thisdir, fileinfo.cFileName);
      sprintf(pathname, "%s\\%s", thisdir, fileinfo.cFileName);
      filename = pathname;
      DoOneFile(desc);
      }


    if (FindNextFile (hdl, &fileinfo) == 0)
      {
      DWORD nErr = GetLastError ();

      if (nErr != ERROR_NO_MORE_FILES)
        {
        fprintf(stderr, "*** %s : ", thisdir);    
        PrintWin32Error (nErr);
        }


      FindClose (hdl);

      // if level == 1 ...
        mb = (double) *totalsize / 1024.0 / 1024.0;

      free (thisdir);
      return;
      }

    } while (1);

  };


int ForAllFiles(int level, char *param, char *desc) {
  __int64 total = 0;
  long    files = 0;

  char starting_point [_MAX_PATH] = "c:";
  
  if ((param != NULL) && (*param != '\0')) strcpy (starting_point, param);

  // remove trailing backslash from starting point
  if (starting_point [strlen (starting_point) - 1] == '\\')
    starting_point [strlen (starting_point) - 1] = 0;

  process_dir (starting_point, "", 0, &total, &files, desc);

  return 0;

} 

#endif /* OLDSTYLE */
#endif /* not __unix__ , i.e. Windows */

int main (int argc, char **argv)
{
   int c;
   char *s, *desc;

   progname = (((s=strrchr(argv[0], '/')) != NULL ? s+1:
               (s=strrchr(argv[0], '\\')) != NULL ? s+1:
               argv[0]));

   if ((argc == 3) && (strcmp(argv[1], "-v") == 0)) {
     verbose = TRUE;
     argc -= 1;
     argv += 1;
   }

   if (argc == 2) {
     desc = "";
   } else if (argc == 3) {
     desc = argv[2];
   } else {
     fprintf(stderr, "syntax: %s [-v] rootdir [\"diskname or description\"]\n", progname);
     exit(EXIT_FAILURE);
   }

   /****************** LOOP OVER ALL FILES ****************/
   (void)ForAllFiles(0, argv[1], desc);
   /************   END OF LOOP OVER ALL FILES   ************/

   exit(EXIT_SUCCESS);
   if (version != NULL) return(EXIT_FAILURE);
}