zeya team mailing list archive
-
zeya team
-
Mailing list archive
-
Message #00048
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