← Back to team overview

zeya team mailing list archive

FEATURE/PATCH: Support m3u playlists as backend

 

Hi Phil, all:

Pls. find attched the m3u.py file implementing the m3u backend and patches to existing files.

I have defined the following format for using m3u playlists:

./zeya.py --backend=playlist --path=/home/r00t/Music/Songbird/Highest\ Rated.m3u --port=9090

Find a sample m3u list attached.

Going ahead, I will probably add support for PLS files using the same backend and choose the backend class depending upon the playlist file extension.

Pls. try it out and let me know how it looks!

Best,
Amit

--
Journal: http://amitksaha.wordpress.com
µ-blog: http://twitter.com/amitsaha
IRC: cornucopic on #scheme, #lisp, #math, #linux
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Amit Saha
#
# This file is part of Zeya.
#
# Zeya is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Zeya is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Zeya. If not, see <http://www.gnu.org/licenses/>.

# m3u backend

import os
import os.path
import sys
import tagpy

from backends import LibraryBackend
from common import tokenize_filename

TITLE='title'
ARTIST='artist'
ALBUM='album'

class M3uBackend(LibraryBackend):
    
    def __init__(self,file_path=None):
        self.m3u_file = file_path
        
        # check if the file exists at all?
        if not os.path.exists(self.m3u_file):
            print "No m3u file found at %r", self.m3u_file
            print "Please check the m3u file location, or try the --directory switch instead"
            sys.exit(1)
        
        try:
            self._infile = open(self.m3u_file)
        except IOError:
            print "Could not read the playlist (%r)", self.m3u_file
            sys.exit(1)

    def get_library_contents(self):
        #sequence of dicts contanining the metadata of all the songs
        self.library=[] 
        
        #sequence of dicts of filenames
        self.file_list={}
            
        next_index = 0

        for filename in open(self.m3u_file):
            #ignore lines in the m3u file beginning with #
            
            if filename[0] != '#':
                try:

                    metadata = extract_metadata(os.path.abspath(filename.rstrip('\r\n'))) #strip the \r\n at the end of each line
                    metadata['key'] = next_index
                    self.file_list[next_index]= filename
                    self.library.append(metadata)

                    next_index = next_index + 1

                except ValueError:
                    continue

        return self.library

    def get_filename_from_key(self, key):
        return self.file_list[int(key)].rstrip('\r\n')


def extract_metadata(filename, tagpy_module=tagpy):
    # same as in directory.py

    """
    Returns a metadata dictionary (a dictionary {ARTIST: ..., ...}) containing
    metadata (artist, title, and album) for the song in question.

    filename: a string supplying a filename.
    tagpy_module: a reference to the tagpy module. This can be faked out for
    unit testing.
    """
    # tagpy can do one of three things:
    #
    # * Return legitimate data. We'll load that data.
    # * Return None. We'll assume this is a music file but that it doesn't have
    #   metadata. Create an entry for it.
    # * Throw ValueError. We'll assume this is not something we could play.
    #   Don't create an enty for it.

    try:
        tag = tagpy_module.FileRef(filename).tag()
    except:
        raise ValueError("Error reading metadata from %r" % (filename,))
    # If no metadata is available, set the title to be the basename of the
    # file. (We have to ensure that the title, in particular, is not empty
    # since the user has to click on it in the web UI.)
    metadata = {
        TITLE: os.path.basename(filename).decode("UTF-8"),
        ARTIST: '',
        ALBUM: album_name_from_path(tag, filename),
        }
    if tag is not None:
        metadata[ARTIST] = tag.artist
        # Again, do not allow metadata[TITLE] to be an empty string, even if
        # tag.title is an empty string.
        metadata[TITLE] = tag.title or metadata[TITLE]
        metadata[ALBUM] = tag.album or metadata[ALBUM]
        return metadata


def album_name_from_path(tag, filename):
    """
    Returns an appropriate Unicode string to use for the album name if the tag
    is empty.
    """
    if tag is not None and (tag.artist or tag.album):
        return u''
    # Use the trailing components of the path.
    path_components = [x for x in os.path.dirname(filename).split(os.sep) if x]
    if len(path_components) >= 2:
        return os.sep.join(path_components[-2:]).decode("UTF-8")
    elif len(path_components) == 1:
        return path_components[0].decode("UTF-8")
    return u''
diff --git a/decoders.py b/decoders.py
index 91b015f..1e80a66 100644
--- a/decoders.py
+++ b/decoders.py
@@ -56,6 +56,7 @@ def has_decoder(filename):
     If it isn't, print a warning, but only if no previous warning has been
     issued for the same decoder.
     """
+    
     if not is_decoder_registered(filename):
         return False
     extension = get_extension(filename)
diff --git a/directory.py b/directory.py
index 649acca..c16f889 100644
--- a/directory.py
+++ b/directory.py
@@ -186,6 +186,7 @@ def extract_metadata(filename, tagpy_module = tagpy):
     #   metadata. Create an entry for it.
     # * Throw ValueError. We'll assume this is not something we could play.
     #   Don't create an enty for it.
+
     try:
         tag = tagpy_module.FileRef(filename).tag()
     except:
diff --git a/options.py b/options.py
index 1bd170b..aba8b6a 100644
--- a/options.py
+++ b/options.py
@@ -28,7 +28,7 @@ DEFAULT_PORT = 8080
 DEFAULT_BITRATE = 64 #kbits/s
 DEFAULT_BACKEND = 'dir'
 
-valid_backends = ['rhythmbox', 'dir']
+valid_backends = ['rhythmbox', 'dir', 'playlist']
 
 class BadArgsError(Exception):
     """
@@ -70,6 +70,7 @@ def get_options(remaining_args):
             help_msg = True
         if flag in ("--backend",):
             backend_type = value
+            print "Backend", backend_type
             if backend_type not in valid_backends:
                 raise BadArgsError("Unsupported backend type %r"
                                    % (backend_type,))
@@ -92,11 +93,13 @@ def get_options(remaining_args):
                 port = int(value)
             except ValueError:
                 raise BadArgsError("Invalid port setting %r" % (value,))
-    if backend_type != 'dir' and path is not None:
+    if backend_type != 'dir' and  backend_type != 'playlist' and path is not None:
         print "Warning: --path was set but is ignored for --backend=%s" \
             % (backend_type,)
     if backend_type == 'dir' and path is None:
         path = "."
+    if backend_type == 'playlist' and path is None:
+        raise BadArgsError("Specify --path for playlist backend")
     return (help_msg, backend_type, bitrate, port, path, basic_auth_file)
 
 def print_usage():
@@ -111,9 +114,13 @@ Options:
       Specify the backend to use. Acceptable values:
         dir: (default) read a directory's contents recursively; see --path
         rhythmbox: read from current user's Rhythmbox library
+        playlist: read from the user specified playlist (m3u) file;
+                  (the playlist location will be given with --path, 
+                   which if missing should result in an error)
 
   --path=PATH
       Directory in which to look for music, under --backend=dir. (Default: ./)
+      Absolute file path, under --backend=playlist
 
   -b, --bitrate=N
       Specify the bitrate for output streams, in kbits/sec. (default: 64)
diff --git a/zeya.py b/zeya.py
index 7ce4224..2a2ee62 100755
--- a/zeya.py
+++ b/zeya.py
@@ -308,6 +308,9 @@ def get_backend(backend_type):
     elif backend_type == 'dir':
         from directory import DirectoryBackend
         return DirectoryBackend(path)
+    elif backend_type == 'playlist':
+        from m3u import M3uBackend
+        return M3uBackend(path)
     else:
         raise ValueError("Invalid backend %r" % (backend_type,))
 
@@ -319,6 +322,7 @@ def run_server(backend, port, bitrate, basic_auth_file=None):
     library_contents = \
         [ s for s in library_contents \
               if decoders.has_decoder(backend.get_filename_from_key(s['key'])) ]
+
     if not library_contents:
         print "WARNING: no tracks were found. Check that you've specified " \
             + "the right backend/path."

Attachment: Highest Rated.m3u
Description: audio/mpegurl


Follow ups