Completed
Branch dev-4.1 (fc3790)
by Felipe A.
01:10
created

M3UFile.iter_files()   B

Complexity

Conditions 7

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
c 2
b 1
f 0
dl 0
loc 15
rs 7.3333
1
2
import sys
3
import codecs
4
import os.path
5
6
from flask._compat import with_metaclass
7
from werkzeug.utils import cached_property
8
from browsepy.compat import walk, range, str_base, PY_LEGACY
9
from browsepy.file import File, underscore_replace, check_under_base
10
11
12
if PY_LEGACY:
13
    import ConfigParser as configparser
14
else:
15
    import configparser
16
17
18
mimetypes = {
19
    'mp3': 'audio/mpeg',
20
    'ogg': 'audio/ogg',
21
    'wav': 'audio/wav',
22
    'm3u': 'audio/x-mpegurl',
23
    'm3u8': 'audio/x-mpegurl',
24
    'pls': 'audio/x-scpls',
25
}
26
27
28
class PlayableFile(File):
29
    parent_class = File
30
    media_map = {
31
        'audio/mpeg': 'mp3',
32
        'audio/ogg': 'ogg',
33
        'audio/wav': 'wav',
34
    }
35
    mimetypes = tuple(media_map)
36
37
    def __init__(self, duration=None, title=None, **kwargs):
38
        self.duration = duration
39
        self.title = title
40
        super(PlayableFile, self).__init__(**kwargs)
41
42
    @property
43
    def title(self):
44
        return self._title or self.name
45
46
    @title.setter
47
    def title(self, title):
48
        self._title = title
49
50
    @property
51
    def media_format(self):
52
        return self.media_map[self.type]
53
54
55
class MetaPlayListFile(type):
56
    def __init__(cls, name, bases, nmspc):
57
        '''
58
        Abstract-class mimetype-based implementation registration on nearest
59
        abstract parent.
60
        '''
61
        type.__init__(cls, name, bases, nmspc)
62
        if cls.abstract_class is None:
63
            cls.specific_classes = {}
64
            cls.abstract_class = cls
65
        elif isinstance(cls.mimetype, str_base):
66
            cls.abstract_class.specific_classes[cls.mimetype] = cls
67
68
69
class PlayListFile(with_metaclass(MetaPlayListFile, File)):
70
    abstract_class = None
71
    playable_class = PlayableFile
72
    mimetypes = ('audio/x-mpegurl', 'audio/x-scpls')
73
74
    def __new__(cls, *args, **kwargs):
75
        '''
76
        Polimorfic mimetype-based constructor
77
        '''
78
        self = super(PlayListFile, cls).__new__(cls)
79
        if cls is cls.abstract_class:
80
            self.__init__(*args, **kwargs)
81
            if self.mimetype in cls.abstract_class.specific_classes:
82
                return cls.specific_classes[self.mimetype](*args, **kwargs)
83
        return self
84
85
    def iter_files(self):
86
        return
87
        yield
88
89
    def normalize_playable_path(self, path):
90
        if not os.path.isabs(path):
91
            path = os.path.normpath(os.path.join(self.parent.path, path))
92
        if check_under_base(path, self.app.config['directory_base']):
93
            return path
94
        return None
95
96
97
class PLSFile(PlayListFile):
98
    ini_parser_cls = (
99
        configparser.SafeConfigParser
100
        if hasattr(configparser, 'SafeConfigParser') else
101
        configparser.ConfigParser
102
        )
103
    maxsize = getattr(sys, 'maxsize', None) or getattr(sys, 'maxint', None)
104
    mimetype = 'audio/x-scpls'
105
106
    @cached_property
107
    def _parser(self):
108
        parser = self.ini_parser()
109
        parser.read(self.path)
110
        return parser
111
112
    def iter_files(self):
113
        maxsize = self._parser.getint('playlist', 'NumberOfEntries', None)
114
        for i in range(self.maxsize if maxsize is None else maxsize):
115
            pf = self.playable_class(
116
                path=self.normalize_playable_path(
117
                    self._parser.get('playlist', 'File%d' % i, None)
118
                    ),
119
                duration=self._parser.getint('playlist', 'Length%d' % i, None),
120
                title=self._parser.get('playlist', 'Title%d' % i, None),
121
                )
122
            if pf.path:
123
                yield pf
124
            elif maxsize is None:
125
                break
126
127
128
class M3UFile(PlayListFile):
129
    mimetype = 'audio/x-mpegurl'
130
131
    def _extract_line(self, line, file=None):
132
        if line.startswith('#EXTINF:'):
133
            duration, title = line.split(',', 1)
134
            file.duration = None if duration == '-1' else int(duration)
135
            file.title = title
136
            return False
137
        file.path = self.normalize_playable_path(line)
138
        return file.path is not None
139
140
    def iter_files(self):
141
        prefix = '#EXTM3U\n'
142
        encoding = 'utf-8' if self.path.endswith('.m3u8') else 'ascii'
143
        with codecs.open(
144
          self.path, 'r',
145
          encoding=encoding,
146
          errors=underscore_replace
147
          ) as f:
148
            if f.read(len(prefix)) == prefix:
149
                pf = PlayableFile()
150
                for line in f:
151
                    line = line.rstrip('\n', 1)
152
                    if line and self._extract_line(line, pf):
153
                        yield pf
154
                        pf = PlayableFile()
155
156
157
class PlayableDirectory(PlayListFile):
158
    @classmethod
159
    def detect(self, file):
160
        max_level = file.path.rstrip('/').count('/') + 5
161
        for root, directories, files in walk(file.path, followlinks=True):
162
            for filename in files:
163
                if filename.rsplit('.', 1)[-1] in mimetypes:
164
                    return True
165
            if root.rstrip('/').count('/') >= max_level:
166
                del directories[:]
167