Welcome, guest | Sign In | My Account | Store | Cart
#!/usr/bin/env python
#-*- coding:utf-8 -*-

#---------
# IMPORT
#---------
import mimetypes, argparse, os, subprocess, re, urllib, sys, \
    errno, logging

#---------
# DEFINE
#---------
description = """
Generate an m3u playlist searching recursively
for multimedia files (video or audio) in the given
directory.
Information from ID3 tags will be extracted for audio
files with FFmpeg available.
"""

__doc__     = description 
__author__  = "Xian Jacobs <manobastardo@gmail.com>"
__date__    = "24 11 2013"
__version__ = "0.0"

options = [
    {
        "options": ["directory"],
        "help"  :
            "Directory to search recursively for multimedia "
            "files."
    },
    {
        "options": ["-p", "--play"],
        "action" : "store_true",
        "help"   :
            "Open the playlist located in the output "
            "directory."
    },
    {
        "options": ["-r", "--relative"],
        "action" : "store_true",
        "help"   :
            "Generate the playlist with local pathnames "
            "relative to the M3U file location."
    },
    {
        "options": ["-s", "--simulate"],
        "action" : "store_true",
        "help"   :
            "Dry run, don't write the playlist to a file."
    },
    {
        "options": ["-v", "--verbose"],
        "action" : "store_true",
        "help"   :
            "Print the files that would be used to generate "
            "the playlist."
    },
    {
        "options": ["-d", "--output-directory"],
        "action" : "store",
        "help"   : "Specify the output directory."
    },
    {
        "options": ["-f", "--output-filename"],
        "action" : "store",
        "help"   : "Specify the output file name."
    }
]

MARKER_FORMAT = "#EXTM3U"
MARKER_RECORD = "#EXTINF"

FORMAT_FILENAME = "{name}.m3u"
FORMAT_M3U      = "{marker}\n{playlist}"
FORMAT_INFO     = "{marker}:{duration},{artist} - {title}"
FORMAT_IOERROR  = "Invalid Output Directories {directories}"

REGEX_DURATION = re.compile(
    r"(.*Duration: )([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])",
    re.MULTILINE|re.DOTALL
)

stripnulls    = lambda data: data.replace("\00", "").strip()
filetitle     = lambda filePath: os.path.splitext(os.path.basename(filePath))[0]
alphatonum    = lambda text: int(text) if text.isdigit() else text 
alphanumkey   = lambda key: [alphatonum(c) for c in re.split("([0-9]+)", key)]

def sortAlphanum(items, index=0):
    return sorted(items, key=lambda x: alphanumkey(x[index]))

def timeSeconds(time):
    return int(sum(
        float(x) * 60 ** i
        for i,x in enumerate(reversed(time.split(":")))
    ))

def isProgramAvailable(program):
    def isExe(filePath):
        return all([
            os.path.isfile(filePath),
            os.access(filePath, os.X_OK)
        ])

    def isRunnable(program):
        try:
            subprocess.Popen(
                [program],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )

        except (OSError, e):
            if e.errno == errno.ENOENT:
                return False

            raise

        return True

    def isAvailable(program):
        if isExe(program) and isRunnable(program):
            return True

        return False

    filePath, fileName = os.path.split(program)

    if filePath:
        if isAvailable(program):
            return True

    else:
        for path in os.environ["PATH"].split(os.pathsep):
            path        = path.strip('"')
            filePathExe = os.path.join(path, program)

            if isAvailable(filePathExe):
                return True

    logging.warning("Program Missing: {0}".format(program))

    return False

class Playlist(object):
    extensionsInclude = [".f4v"]
    extensionsExclude = [".m3u"]

    fieldsTag  = {
        "title"   : (  3,  33, stripnulls),
        "artist"  : ( 33,  63, stripnulls),
        "album"   : ( 63,  93, stripnulls),
        "year"    : ( 93,  97, stripnulls),
        "comment" : ( 97, 126, stripnulls),
        "genre"   : (127, 128, ord)
    }

    def __init__(self, arguments):
        self.rootDirectory     = None
        self.arguments         = arguments
        self.ffmpegIsAvailable = isProgramAvailable("ffmpeg")

        mimetypes.init()

        self.extensions = self.extensionsInclude + [
            extension
            for extension in mimetypes.types_map
            if mimetypes.types_map[extension].split("/")[0]
                in ["video", "audio"]
        ]

    def generate(self, pathDirectory):
        self.rootDirectory = pathDirectory
        playlistItems      = self._getPlaylistItems(pathDirectory)
        playlistFilePath   = self.getFileOutput(pathDirectory)

        if self.arguments.verbose:
            self.printPlaylistItems(playlistItems)

        if not self.arguments.simulate:
            self.writePlaylistItems(playlistFilePath, playlistItems)

        if all([
            self.arguments.play,
            os.path.exists(playlistFilePath),
            os.path.isfile(playlistFilePath)
        ]):
            self.openPlaylist(playlistFilePath)

    def openPlaylist(self, playlistFilePath):
        if sys.platform.startswith('darwin'):
            subprocess.Popen(('open', playlistFilePath))

        elif os.name == 'nt':
            os.startfile(playlistFilePath)

        elif os.name == 'posix':
            subprocess.Popen(('xdg-open', playlistFilePath))

    def printPlaylistItems(self, playlistItems):
        playlist       = playlistItems["playlist"]
        playlistSorted = self.sortPlaylist(playlist)

        for playlistInfo in playlistSorted:
            print(playlistInfo[1])

    def _getPlaylistItems(self, pathDirectory):
        return {
            "marker"   : MARKER_FORMAT,
            "playlist" : self.getPlaylistInfo(pathDirectory)
        }

    def writePlaylistItems(self, filePath, items):
        with open(filePath, "w") as fileOutput:
            fileOutput.write(self.formatPlaylistItems(items))

    def sortPlaylist(self, playlist):
        return sortAlphanum(playlist, 1)

    def formatPlaylistItems(self, playlistItems):
        playlistFormatted = map(
            lambda tags: "\n".join(tags),
            playlistItems["playlist"]
        )

        playlistItems["playlist"] = "\n".join(playlistFormatted)

        return FORMAT_M3U.format(**playlistItems)

    def flattenPlaylist(self, playlist):
        return sum(playlist, [])

    def getPlaylistInfo(self, pathDirectory):
        playlist       = self.getPlaylistsInfos(pathDirectory)
        playlistFlat   = self.flattenPlaylist(playlist)
        playlistSorted = self.sortPlaylist(playlistFlat)

        return playlistSorted

    def getPlaylistsInfos(self, pathDirectory):
        return [
            self._formatTags(tags)
            for tags in self._getInfoTags(pathDirectory)
        ] 

    def getDirectoryOutput(self, directoryPaths):
        for directoryPath in directoryPaths:
            if  directoryPath != None \
            and os.path.exists(directoryPath) \
            and os.path.isdir(directoryPath):
                return directoryPath

        raise (IOError, FORMAT_IOERROR.format(directories=directoryPaths))
        
    def getNameOutput(self, directoryPlaylist):
        if self.arguments.output_filename != None:
            name = self.arguments.output_filename

        else:
            name = os.path.basename(directoryPlaylist)

        return FORMAT_FILENAME.format(name=name)

    def getFileOutput(self, pathDirectory):
        directoryPlaylist = pathDirectory.rstrip(os.path.sep)
        directoryOutput   = self.getDirectoryOutput([
            self.arguments.output_directory,
            directoryPlaylist,
        ])
        fileName      = self.getNameOutput(directoryPlaylist)
        filePath      = os.path.join(directoryOutput, fileName)

        return filePath

    def _formatTags(self, tags):
        return [
            (FORMAT_INFO.format(**fileTags), fileTags["path"])
            for fileTags in tags
        ]

    def _getInfoTags(self, pathDirectory):
        tagsFromFilePaths = [
            self._getTagsFromFileNames(directoryPath, fileNames)
            for directoryPath, directoryNames, fileNames in os.walk(pathDirectory)
        ]

        return tagsFromFilePaths

    def _getTagsFromFileNames(self, directoryPath, fileNames):
        tagsFromFilePaths = filter(
            lambda fileTag: fileTag != None,
            self.getTagsFromFileNames(directoryPath, fileNames)
        )

        return tagsFromFilePaths

    def getTagsFromFileNames(self, directoryPath, fileNames):
        return [
            self._getTagsFromFileName(directoryPath, fileName)
            for fileName in fileNames
        ]

    def _getTagsFromFileName(self, directoryPath, fileName):
        fileTitle, fileExtension = os.path.splitext(fileName)

        tagsFromFilePath = (
            self.getTagsFromFileName(os.path.join(directoryPath, fileName))
            if all([
                fileExtension in self.extensions,
                fileExtension not in self.extensionsExclude
            ])
            else None
        )

        return tagsFromFilePath

    def getMimeType(self, filePath):
        #fileUrl      = urllib.pathname2url(filePath)
        fileUrl      = "file:///{0}".format(filePath)
        fileMimeType = mimetypes.guess_type(fileUrl)[0]

        return (
            False
            if fileMimeType == None
            else fileMimeType.split("/")[0]
        )

    def getTagData(self, filePath):
        if self.getMimeType(filePath) != "audio":
            return None

        with open(filePath, "rb", 0) as fileInput:
            fileInput.seek(-128, 2)  
  
            return fileInput.read(128)

    def getTagsFromFileName(self, filePath):
        tagData = self.getTagData(filePath)

        tagsFromFilePath = dict(
            self._validateTagData(tag, start, end, parseFunc, tagData)
            if tagData != None
            else (tag, "")
            for tag, (start, end, parseFunc) in self.fieldsTag.items()
        )

        return self._formatTagsFromFilePath(filePath, tagsFromFilePath)

    def _validateTagData(self, tag, start, end, parseFunc, tagData):
        return (
            (tag, re.sub("[\n\t]", " ", str(parseFunc(tagData[start:end]))))
            if not self.validateTagData(tagData)
            else (tag, "")
        )

    def validateTagData(self, tagData):
        if "Reference&#32" in tagData:
            return False

        return self.checkEncoding(tagData) != ""

    def runFFmpeg(self, filePath):
        process = subprocess.Popen(
            ["ffmpeg", "-i", filePath],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT
        )

        stdout, stderr = process.communicate()

        return stdout

    def formatTime(self, time):
        try:
            return str(timeSeconds(REGEX_DURATION.match(time).group(2)))

        except AttributeError:
            return 0

    def getDuration(self, filePath):
        if not self.ffmpegIsAvailable:
            return 0

        time = self.runFFmpeg(filePath)

        return self.formatTime(time)

    def checkTagTitle(self, tagTitle, filePath):
        return (
            tagTitle
            if self.checkEncoding(tagTitle) != ""
            else filetitle(filePath)
        )

    def checkTagArtist(self, tagArtist):
        return (
            tagArtist
            if self.checkEncoding(tagArtist) != ""
            else os.path.basename(self.rootDirectory).strip()
        )

    def checkEncoding(self, text):
        #try:
        #    text.decode("utf-8")

        #except UnicodeDecodeError:
        #    return ""

        return text.strip()

    def getTagPath(self, filePath):
        filePathChild = re.sub(r"^"+re.escape(self.rootDirectory), "", filePath)

        return (
            filePathChild.lstrip(os.path.sep)
            if self.arguments.relative
            else filePath
        )

    def _formatTagsFromFilePath(self, filePath, tagsFromFilePath):
        tags = {
            "marker"   : MARKER_RECORD,
            "path"     : self.getTagPath(filePath),
            "duration" : self.getDuration(filePath),
            "title"    : self.checkTagTitle(tagsFromFilePath["title"], filePath),
            "artist"   : self.checkTagArtist(tagsFromFilePath["artist"])
        }

        tagsFromFilePath.update(tags)

        return tagsFromFilePath

class Parser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
       super(Parser, self).__init__(*args, **kwargs)

    def addOption(self, option):
        options = option["options"]
        option.pop("options")

        self.add_argument(*options, **option)
        
    def parseOptions(self, options):
        for option in options:
            self.addOption(option)
            
        return super(Parser, self).parse_args()
    
#---------
# MAIN
#---------
#if __name__ == "__main__":
parser    = Parser(description=description)
arguments = parser.parseOptions(options)

if all([
	os.path.exists(arguments.directory),
	os.path.isdir(arguments.directory)
]):
	playlist = Playlist(arguments)
	playlist.generate(arguments.directory)

Diff to Previous Revision

--- revision 7 2013-11-26 18:32:54
+++ revision 8 2014-01-07 20:18:04
@@ -4,7 +4,8 @@
 #---------
 # IMPORT
 #---------
-import mimetypes, argparse, os, subprocess, re, urllib, sys, errno, logging
+import mimetypes, argparse, os, subprocess, re, urllib, sys, \
+    errno, logging
 
 #---------
 # DEFINE
@@ -25,35 +26,46 @@
 options = [
     {
         "options": ["directory"],
-        "help"  : "Directory to search multimedia files"
+        "help"  :
+            "Directory to search recursively for multimedia "
+            "files."
     },
     {
         "options": ["-p", "--play"],
         "action" : "store_true",
-        "help"   : "Open playlist after generating"
+        "help"   :
+            "Open the playlist located in the output "
+            "directory."
     },
     {
         "options": ["-r", "--relative"],
         "action" : "store_true",
-        "help"   : (
-            "Create with local pathnames relative to "
-            "the M3U file location"
-        )
-    },
-    {
-        "options": ["-n", "--just-print", "--dry-run", "--recon"],
+        "help"   :
+            "Generate the playlist with local pathnames "
+            "relative to the M3U file location."
+    },
+    {
+        "options": ["-s", "--simulate"],
         "action" : "store_true",
-        "help"   : (
-            "Print the files that would be added to the "
-            "playlist, but do not generate it."
-        )
-    },
-    {
-        "options": ["-o", "--just-play"],
+        "help"   :
+            "Dry run, don't write the playlist to a file."
+    },
+    {
+        "options": ["-v", "--verbose"],
         "action" : "store_true",
-        "help"   : (
-            "If playlist exists, open it"
-        )
+        "help"   :
+            "Print the files that would be used to generate "
+            "the playlist."
+    },
+    {
+        "options": ["-d", "--output-directory"],
+        "action" : "store",
+        "help"   : "Specify the output directory."
+    },
+    {
+        "options": ["-f", "--output-filename"],
+        "action" : "store",
+        "help"   : "Specify the output file name."
     }
 ]
 
@@ -63,6 +75,7 @@
 FORMAT_FILENAME = "{name}.m3u"
 FORMAT_M3U      = "{marker}\n{playlist}"
 FORMAT_INFO     = "{marker}:{duration},{artist} - {title}"
+FORMAT_IOERROR  = "Invalid Output Directories {directories}"
 
 REGEX_DURATION = re.compile(
     r"(.*Duration: )([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])",
@@ -98,7 +111,7 @@
                 stderr=subprocess.PIPE
             )
 
-        except OSError, e:
+        except (OSError, e):
             if e.errno == errno.ENOENT:
                 return False
 
@@ -153,7 +166,8 @@
         self.extensions = self.extensionsInclude + [
             extension
             for extension in mimetypes.types_map
-            if mimetypes.types_map[extension].split("/")[0] in ["video", "audio"]
+            if mimetypes.types_map[extension].split("/")[0]
+                in ["video", "audio"]
         ]
 
     def generate(self, pathDirectory):
@@ -161,17 +175,14 @@
         playlistItems      = self._getPlaylistItems(pathDirectory)
         playlistFilePath   = self.getFileOutput(pathDirectory)
 
-        if self.arguments.just_play:
-            pass
-
-        elif self.arguments.just_print:
+        if self.arguments.verbose:
             self.printPlaylistItems(playlistItems)
 
-        else:
+        if not self.arguments.simulate:
             self.writePlaylistItems(playlistFilePath, playlistItems)
 
         if all([
-            self.arguments.play or self.arguments.just_play,
+            self.arguments.play,
             os.path.exists(playlistFilePath),
             os.path.isfile(playlistFilePath)
         ]):
@@ -182,13 +193,14 @@
             subprocess.Popen(('open', playlistFilePath))
 
         elif os.name == 'nt':
-            os.startfile(filepath)
+            os.startfile(playlistFilePath)
 
         elif os.name == 'posix':
             subprocess.Popen(('xdg-open', playlistFilePath))
 
     def printPlaylistItems(self, playlistItems):
-        playlistSorted = self.sortPlaylist(playlistItems["playlist"])
+        playlist       = playlistItems["playlist"]
+        playlistSorted = self.sortPlaylist(playlist)
 
         for playlistInfo in playlistSorted:
             print(playlistInfo[1])
@@ -199,9 +211,9 @@
             "playlist" : self.getPlaylistInfo(pathDirectory)
         }
 
-    def writePlaylistItems(self, playlistFilePath, playlistItems):
-        with open(playlistFilePath, "w") as fileOutput:
-            fileOutput.write(self.formatPlaylistItems(playlistItems))
+    def writePlaylistItems(self, filePath, items):
+        with open(filePath, "w") as fileOutput:
+            fileOutput.write(self.formatPlaylistItems(items))
 
     def sortPlaylist(self, playlist):
         return sortAlphanum(playlist, 1)
@@ -232,10 +244,32 @@
             for tags in self._getInfoTags(pathDirectory)
         ] 
 
+    def getDirectoryOutput(self, directoryPaths):
+        for directoryPath in directoryPaths:
+            if  directoryPath != None \
+            and os.path.exists(directoryPath) \
+            and os.path.isdir(directoryPath):
+                return directoryPath
+
+        raise (IOError, FORMAT_IOERROR.format(directories=directoryPaths))
+        
+    def getNameOutput(self, directoryPlaylist):
+        if self.arguments.output_filename != None:
+            name = self.arguments.output_filename
+
+        else:
+            name = os.path.basename(directoryPlaylist)
+
+        return FORMAT_FILENAME.format(name=name)
+
     def getFileOutput(self, pathDirectory):
-        directoryPath = pathDirectory.rstrip(os.path.sep)
-        fileName      = FORMAT_FILENAME.format(name=os.path.basename(directoryPath))       
-        filePath      = os.path.join(pathDirectory, fileName)
+        directoryPlaylist = pathDirectory.rstrip(os.path.sep)
+        directoryOutput   = self.getDirectoryOutput([
+            self.arguments.output_directory,
+            directoryPlaylist,
+        ])
+        fileName      = self.getNameOutput(directoryPlaylist)
+        filePath      = os.path.join(directoryOutput, fileName)
 
         return filePath
 
@@ -282,7 +316,8 @@
         return tagsFromFilePath
 
     def getMimeType(self, filePath):
-        fileUrl      = urllib.pathname2url(filePath)
+        #fileUrl      = urllib.pathname2url(filePath)
+        fileUrl      = "file:///{0}".format(filePath)
         fileMimeType = mimetypes.guess_type(fileUrl)[0]
 
         return (
@@ -366,11 +401,11 @@
         )
 
     def checkEncoding(self, text):
-        try:
-            text.decode("utf-8")
-
-        except UnicodeDecodeError:
-            return ""
+        #try:
+        #    text.decode("utf-8")
+
+        #except UnicodeDecodeError:
+        #    return ""
 
         return text.strip()
 
@@ -405,22 +440,23 @@
         option.pop("options")
 
         self.add_argument(*options, **option)
-
+        
     def parseOptions(self, options):
-        map(self.addOption, options)
-
+        for option in options:
+            self.addOption(option)
+            
         return super(Parser, self).parse_args()
     
 #---------
 # MAIN
 #---------
-if __name__ == "__main__":
-    parser    = Parser(description=description)
-    arguments = parser.parseOptions(options)
-
-    if all([
-        os.path.exists(arguments.directory),
-        os.path.isdir(arguments.directory)
-    ]):
-        playlist = Playlist(arguments)
-        playlist.generate(arguments.directory)
+#if __name__ == "__main__":
+parser    = Parser(description=description)
+arguments = parser.parseOptions(options)
+
+if all([
+	os.path.exists(arguments.directory),
+	os.path.isdir(arguments.directory)
+]):
+	playlist = Playlist(arguments)
+	playlist.generate(arguments.directory)

History