Completed
Push — dev-4.1-unstable ( 136355...f650fd )
by Felipe A.
01:01
created

PlayableBase   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 26
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 26
rs 10
c 1
b 0
f 0
wmc 5

2 Methods

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