← Back to team overview

zeya team mailing list archive

Re: FEATURE/PATCH: Support m3u playlists as backend

 


Hi Phil:

Phil Sung wrote:
Hi Amit,

(Happy new year! I apologize for the delay in responding. Various
things have been keeping me busy.)

On Thu, Dec 31, 2009 at 04:47, Amit Saha <lists.amitsaha@xxxxxxxxx> wrote:
Amit Saha wrote:
Pls. find attched the m3u.py file implementing the m3u backend and patches
to existing files.

Thanks! In general I think everything basically looks sound. A few comments:

1. Please move extract_metadata and album_name_from_path to
backends.py so both the dir and m3u backends can take advantage of a
single implementation.

Done. Pls. find the patch and the m3u.py file attached.


2. If you use os.path.expanduser (and optionally, also
os.path.abspath), then you can remove the requirement that the user
specify an absolute path to the playlist.

Thanks for that point. I shall do that when I have some time and probably when I add the PLS support.


Hope the patch looks good for a commit!

Best Regards,
Amit

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.

That sounds good to me.

Cheers,
Phil



--
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 *


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=[] 
        
        #dict 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')
diff --git a/backends.py b/backends.py
index 3c3a9ba..9b78dc6 100644
--- a/backends.py
+++ b/backends.py
@@ -23,6 +23,11 @@ import signal
 import socket
 import subprocess
 import time
+import tagpy
+
+TITLE='title'
+ARTIST='artist'
+ALBUM='album'
 
 # For Python2.5 compatibility, we create an equivalent to
 # subprocess.Popen.terminate (new in Python2.6) and patch it in.
@@ -172,3 +177,56 @@ class LibraryBackend():
         #
         # Raise KeyError if the key is not valid.
         raise NotImplementedError()
+
+def extract_metadata(filename, tagpy_module=tagpy):
+    """
+    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..b1f0799 100644
--- a/directory.py
+++ b/directory.py
@@ -27,32 +27,16 @@ import os
 import tagpy
 import pickle
 
-from backends import LibraryBackend
+#import backends
+
+from backends import *
 from common import tokenize_filename
 
 KEY = 'key'
-TITLE = 'title'
-ARTIST = 'artist'
-ALBUM = 'album'
-
 DB = 'db'
 KEY_FILENAME = 'key_filename'
 MTIMES = 'mtimes'
 
-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''
 
 class DirectoryBackend(LibraryBackend):
     """
@@ -169,39 +153,3 @@ class DirectoryBackend(LibraryBackend):
 
     def get_filename_from_key(self, key):
         return self.key_filename[int(key)]
-
-def extract_metadata(filename, tagpy_module = tagpy):
-    """
-    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
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."

Follow ups

References