Completed
Branch dev-4.1 (3cf8c0)
by Felipe A.
01:13
created

PLSFileParser.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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