Completed
Push — dev-4.1 ( c25dff...77a456 )
by Felipe A.
01:14
created

M3UFile._entries()   B

Complexity

Conditions 6

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
c 1
b 0
f 0
dl 0
loc 13
rs 8
1
2
import sys
3
import codecs
4
import os.path
5
6
from browsepy.compat import range, PY_LEGACY
7
from browsepy.file import Node, File, Directory, \
8
                          underscore_replace, check_under_base
9
10
11
if PY_LEGACY:
12
    import ConfigParser as configparser
13
else:
14
    import configparser
15
16
ConfigParserBase = (
17
    configparser.SafeConfigParser
18
    if hasattr(configparser, 'SafeConfigParser') else
19
    configparser.ConfigParser
20
    )
21
22
extensions = {
23
    'mp3': 'audio/mpeg',
24
    'ogg': 'audio/ogg',
25
    'wav': 'audio/wav',
26
    'm3u': 'audio/x-mpegurl',
27
    'm3u8': 'audio/x-mpegurl',
28
    'pls': 'audio/x-scpls',
29
}
30
31
ini_parser_base = (
32
    configparser.SafeConfigParser
33
    if hasattr(configparser, 'SafeConfigParser') else
34
    configparser.ConfigParser
35
    )
36
37
38
class PLSFileParser(ConfigParserBase):
39
    '''
40
    ConfigParser class accepting fallback on get for convenience.
41
    '''
42
    NOT_SET = type('NotSetType', (object,), {})
43
44
    def getint(self, section, key, fallback=NOT_SET, **kwargs):
45
        try:
46
            return super(PLSFileParser, self).getint(section, key, **kwargs)
47
        except (configparser.NoOptionError, ValueError):
48
            if fallback is self.NOT_SET:
49
                raise
50
            return fallback
51
52
    def get(self, section, key, fallback=NOT_SET, **kwargs):
53
        try:
54
            return super(PLSFileParser, self).get(section, key, **kwargs)
55
        except (configparser.NoOptionError, ValueError):
56
            if fallback is self.NOT_SET:
57
                raise
58
            return fallback
59
60
61
class PlayableFile(File):
62
    media_map = {
63
        'audio/mpeg': 'mp3',
64
        'audio/ogg': 'ogg',
65
        'audio/wav': 'wav'
66
    }
67
    mimetypes = tuple(media_map)
68
69
    def __init__(self, **kwargs):
70
        self.duration = kwargs.pop('duration', None)
71
        self.title = kwargs.pop('title', None)
72
        super(PlayableFile, self).__init__(**kwargs)
73
74
    @property
75
    def title(self):
76
        return self._title or self.name
77
78
    @title.setter
79
    def title(self, title):
80
        self._title = title
81
82
    @property
83
    def media_format(self):
84
        return self.media_map[self.type]
85
86
87
class PlayListFile(Directory):
88
    playable_class = PlayableFile
89
    mimetypes = ('audio/x-mpegurl', 'audio/x-scpls')
90
    sortkey = None  # disables listdir sorting
91
92
    @classmethod
93
    def from_urlpath(cls, path, app=None):
94
        original = Node.from_urlpath(path, app)
95
        if original.mimetype == PlayableDirectory.mimetype:
96
            return PlayableDirectory(original.path, original.app)
97
        elif original.mimetype == M3UFile.mimetype:
98
            return M3UFile(original.path, original.app)
99
        if original.mimetype == PLSFile.mimetype:
100
            return PLSFile(original.path, original.app)
101
        return original
102
103
    def normalize_playable_path(self, path):
104
        if '://' in path:
105
            return path
106
        if not os.path.isabs(path):
107
            return os.path.normpath(os.path.join(self.parent.path, path))
108
        if check_under_base(path, self.app.config['directory_base']):
109
            return os.path.normpath(path)
110
        return None
111
112
    def _entries(self):
113
        return
114
        yield
115
116
    def _listdir(self):
117
        for file in self._entries():
118
            if detect_playable_mimetype(file.path):
119
                yield file
120
121
122
class PLSFile(PlayListFile):
123
    ini_parser_class = PLSFileParser
124
    maxsize = getattr(sys, 'maxsize', None) or getattr(sys, 'maxint', None)
125
    mimetype = 'audio/x-scpls'
126
127
    def _entries(self):
128
        parser = self.ini_parser_class()
129
        parser.read(self.path)
130
        maxsize = parser.getint('playlist', 'NumberOfEntries', None)
131
        for i in range(1, (self.maxsize if maxsize is None else maxsize) + 1):
132
            path = parser.get('playlist', 'File%d' % i, None)
133
            if not path:
134
                if maxsize:
135
                    continue
136
                break
137
            path = self.normalize_playable_path(path)
138
            if not path:
139
                continue
140
            yield self.playable_class(
141
                path=path,
142
                app=self.app,
143
                duration=parser.getint(
144
                    'playlist', 'Length%d' % i,
145
                    None
146
                    ),
147
                title=parser.get(
148
                    'playlist',
149
                    'Title%d' % i,
150
                    None
151
                    ),
152
                )
153
154
155
class M3UFile(PlayListFile):
156
    mimetype = 'audio/x-mpegurl'
157
158
    def _iter_lines(self):
159
        prefix = '#EXTM3U\n'
160
        encoding = 'utf-8' if self.path.endswith('.m3u8') else 'ascii'
161
        with codecs.open(
162
          self.path, 'r',
163
          encoding=encoding,
164
          errors=underscore_replace
165
          ) as f:
166
            if f.read(len(prefix)) != prefix:
167
                f.seek(0)
168
            for line in f:
169
                line = line.rstrip('\n')
170
                if line:
171
                    yield line
172
173
    def _entries(self):
174
        data = {}
175
        for line in self._iter_lines():
176
            if line.startswith('#EXTINF:'):
177
                duration, title = line.split(',', 1)
178
                data['duration'] = None if duration == '-1' else int(duration)
179
                data['title'] = title
180
            if not line:
181
                continue
182
            path = self.normalize_playable_path(line)
183
            if path:
184
                yield self.playable_class(path=path, app=self.app, **data)
185
            data.clear()
186
187
188
class PlayableDirectory(Directory):
189
    @classmethod
190
    def detect(cls, node):
191
        if node.is_directory:
192
            for file in node._listdir():
193
                if file.name.rsplit('.', 1)[-1] in extensions:
194
                    return True
195
        return False
196
197
    def _listdir(self):
198
        for file in super(PlayableDirectory, self)._listdir():
199
            if detect_playable_mimetype(file.path):
200
                yield file
201
202
203
def detect_playable_mimetype(path, os_sep=os.sep):
204
    basename = path.rsplit(os_sep)[-1]
205
    if '.' in basename:
206
        ext = basename.rsplit('.')[-1]
207
        return extensions.get(ext, None)
208
    return None
209