Completed
Push — master ( d6e75b...42acea )
by Felipe A.
01:05
created

PlayableDirectory.entries()   A

Complexity

Conditions 3

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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