Completed
Push — 0.5.3 ( c4898a...1367b0 )
by Felipe A.
06:03
created

PlayListFile.normalize_playable_path()   B

Complexity

Conditions 6

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 8
cc 6
1
2
import sys
3
import codecs
4
import os.path
5
import warnings
6
7
from browsepy.compat import range, PY_LEGACY  # noqa
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
        path = os.path.normpath(path)
132
        if not os.path.isabs(path):
133
            return os.path.join(self.parent.path, path)
134
        if self.drive and not os.path.splitdrive(path)[0]:
135
            path = self.drive + path
136
        if check_under_base(path, self.app.config['directory_base']):
137
            return path
138
        return None
139
140
    def _entries(self):
141
        return
142
        yield  # noqa
143
144
    def entries(self):
145
        for file in self._entries():
146
            if PlayableFile.detect(file):
147
                yield file
148
149
150
class PLSFile(PlayListFile):
151
    ini_parser_class = PLSFileParser
152
    maxsize = getattr(sys, 'maxsize', None) or getattr(sys, 'maxint', None)
153
    mimetype = 'audio/x-scpls'
154
    extensions = PlayableBase.extensions_from_mimetypes([mimetype])
155
156
    def _entries(self):
157
        parser = self.ini_parser_class(self.path)
158
        maxsize = parser.getint('playlist', 'NumberOfEntries', None)
159
        for i in range(1, self.maxsize if maxsize is None else maxsize + 1):
160
            path = parser.get('playlist', 'File%d' % i, None)
161
            if not path:
162
                if maxsize:
163
                    continue
164
                break
165
            path = self.normalize_playable_path(path)
166
            if not path:
167
                continue
168
            yield self.playable_class(
169
                path=path,
170
                app=self.app,
171
                duration=parser.getint(
172
                    'playlist', 'Length%d' % i,
173
                    None
174
                    ),
175
                title=parser.get(
176
                    'playlist',
177
                    'Title%d' % i,
178
                    None
179
                    ),
180
                )
181
182
183
class M3UFile(PlayListFile):
184
    mimetype = 'audio/x-mpegurl'
185
    extensions = PlayableBase.extensions_from_mimetypes([mimetype])
186
187
    def _iter_lines(self):
188
        prefix = '#EXTM3U\n'
189
        encoding = 'utf-8' if self.path.endswith('.m3u8') else 'ascii'
190
        with codecs.open(
191
          self.path, 'r',
192
          encoding=encoding,
193
          errors=underscore_replace
194
          ) as f:
195
            if f.read(len(prefix)) != prefix:
196
                f.seek(0)
197
            for line in f:
198
                line = line.rstrip()
199
                if line:
200
                    yield line
201
202
    def _entries(self):
203
        data = {}
204
        for line in self._iter_lines():
205
            if line.startswith('#EXTINF:'):
206
                duration, title = line.split(',', 1)
207
                data['duration'] = None if duration == '-1' else int(duration)
208
                data['title'] = title
209
            if not line:
210
                continue
211
            path = self.normalize_playable_path(line)
212
            if path:
213
                yield self.playable_class(path=path, app=self.app, **data)
214
            data.clear()
215
216
217
class PlayableDirectory(Directory):
218
    file_class = PlayableFile
219
    name = ''
220
221
    @property
222
    def parent(self):
223
        return super(PlayableDirectory, self)  # parent is self as directory
224
225
    @classmethod
226
    def detect(cls, node):
227
        if node.is_directory:
228
            for file in node._listdir():
229
                if PlayableFile.detect(file):
230
                    return cls.mimetype
231
        return None
232
233
    def entries(self):
234
        for file in super(PlayableDirectory, self)._listdir():
235
            if PlayableFile.detect(file):
236
                yield file
237
238
239
def detect_playable_mimetype(path, os_sep=os.sep):
240
    basename = path.rsplit(os_sep)[-1]
241
    if '.' in basename:
242
        ext = basename.rsplit('.')[-1]
243
        return PlayableBase.extensions.get(ext, None)
244
    return None
245