Completed
Branch dev-4.1 (d7c5dd)
by Felipe A.
01:36
created

PlayListFile.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
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 PlayableFile(File):
58
    media_map = {
59
        'audio/mpeg': 'mp3',
60
        'audio/ogg': 'ogg',
61
        'audio/wav': 'wav'
62
    }
63
    extensions = {
64
        'mp3': 'audio/mpeg',
65
        'ogg': 'audio/ogg',
66
        'wav': 'audio/wav',
67
    }
68
    mimetypes = tuple(frozenset(extensions.values()))
69
70
    def __init__(self, **kwargs):
71
        self.duration = kwargs.pop('duration', None)
72
        self.title = kwargs.pop('title', None)
73
        super(PlayableFile, self).__init__(**kwargs)
74
75
    @property
76
    def title(self):
77
        return self._title or self.name
78
79
    @title.setter
80
    def title(self, title):
81
        self._title = title
82
83
    @property
84
    def media_format(self):
85
        return self.media_map[self.type]
86
87
88
class PlayListFile(File):
89
    playable_class = PlayableFile
90
    extensions = {
91
        'm3u': 'audio/x-mpegurl',
92
        'm3u8': 'audio/x-mpegurl',
93
        'pls': 'audio/x-scpls',
94
    }
95
    mimetypes = tuple(frozenset(extensions.values()))
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 entries(self):
122
        for file in self._entries():
123
            if detect_audio_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
    file_class = PlayableFile
194
195
    @classmethod
196
    def detect(cls, node):
197
        if node.is_directory:
198
            for file in node._listdir():
199
                if detect_audio_mimetype(file.path):
200
                    return True
201
        return False
202
203
    def entries(self):
204
        for file in super(PlayableDirectory, self)._listdir():
205
            if detect_audio_mimetype(file.path):
206
                yield file
207
208
209
def detect_playable_mimetype(path, os_sep=os.sep):
210
    return (
211
        detect_audio_mimetype(path, os_sep) or
212
        detect_playlist_mimetype(path, os_sep)
213
        )
214
215
216
def detect_audio_mimetype(path, os_sep=os.sep):
217
    basename = path.rsplit(os_sep)[-1]
218
    if '.' in basename:
219
        ext = basename.rsplit('.')[-1]
220
        return PlayableFile.extensions.get(ext, None)
221
    return None
222
223
224
def detect_playlist_mimetype(path, os_sep=os.sep):
225
    basename = path.rsplit(os_sep)[-1]
226
    if '.' in basename:
227
        ext = basename.rsplit('.')[-1]
228
        return PlayListFile.extensions.get(ext, None)
229
    return None
230