must

Форк
0
/
main.py 
433 строки · 18.6 Кб
1
import os
2
import sys
3
import time
4
import datetime
5
import json
6
import random
7
import ctypes
8
import log
9
import com_base
10
import com_tcp
11
import com_udp
12
import backend_base
13
import backend_winmm
14
import backend_sdl2
15
import backend_fmodex
16

17

18
class App:
19
    def __init__(self, argv: any) -> None:
20
        self.exit_code = 1
21
        self.first_arg = argv[0]
22
        self.argv = argv[1:]
23
        self.is_le = sys.byteorder == 'little'
24
        self.cwd = os.path.dirname(__file__) or os.getcwd()
25
        self.encoding = 'utf-8'
26
        if sys.platform == 'win32':
27
            self.sig_kill = 0
28
            self.paths = [self.cwd] + (os.getenv('PATH') or '').split(';')
29
            self.auto_postfix = ''
30
            self.auto_prefix = ''
31
            self.load_library = ctypes.windll.LoadLibrary
32
        else:
33
            self.sig_kill = 9
34
            self.auto_postfix = '.so'
35
            self.auto_prefix = 'lib'
36
            self.load_library = ctypes.CDLL
37
            self.paths = [self.cwd] + (os.getenv('LD_LIBRARY_PATH') or '').split(':')\
38
                + (os.getenv('PATH') or '').split(':')
39
        self.config_path = os.path.join(self.cwd, 'config.json')
40
        if not os.path.isfile(self.config_path):
41
            self.write_json(
42
                os.path.join(self.cwd, 'config.json'), self.read_json(os.path.join(self.cwd, 'default_config.json'))
43
            )
44
        self.config = self.read_json(self.config_path)
45
        log.enable_logging = self.config['allow_logging']
46
        try:
47
            if '--client-only' in self.argv or (self.config['need_server_arg'] and '--server-only' not in self.argv):
48
                raise RuntimeError('Client Only!')
49
            if self.config['com_type'] == 'tcp':
50
                self.server: com_base.BaseServer = com_tcp.TCPServer(self)
51
            elif self.config['com_type'] == 'udp':
52
                self.server: com_base.BaseServer = com_udp.UDPServer(self)
53
            else:
54
                raise FileNotFoundError('Unknown communication type')
55
        except RuntimeError:
56
            if '--server-only' in self.argv:
57
                raise RuntimeError('Server Only!')
58
            if self.config['com_type'] == 'tcp':
59
                self.client: com_base.BaseClient = com_tcp.TCPClient(self)
60
            elif self.config['com_type'] == 'udp':
61
                self.client: com_base.BaseClient = com_udp.UDPClient(self)
62
            else:
63
                raise FileNotFoundError('Unknown communication type')
64
            if self.argv and not (len(self.argv) <= 1 and self.argv[0] == '--client-only'):
65
                self.client.send(';'.join(self.argv))
66
                self.exit_code = 0
67
                # self.client.send('disconnect')
68
            else:
69
                self.client_prompt()
70
            self.client.destroy()
71
            # self.exit_code = 0
72
            return
73
        if self.config['audio_backend'] == 'sdl2':
74
            self.search_libs('libopusfile-0', 'libopus-0', 'libogg-0', 'libmodplug-1')
75
            self.bk: backend_base.BaseBackend = backend_sdl2.SDL2Backend(
76
                self, self.search_libs('SDL2', 'SDL2_mixer', prefix=self.auto_prefix)
77
            )
78
        elif self.config['audio_backend'] == 'fmod':
79
            if sys.platform == 'win32':
80
                self.search_libs('VCRUNTIME140_APP')
81
            self.search_libs('libfsbvorbis64')
82
            self.bk: backend_base.BaseBackend = backend_fmodex.FmodExBackend(
83
                self, self.search_libs('opus', 'media_foundation', 'fsbank', 'fmod', prefix=self.auto_prefix)
84
            )
85
        elif self.config['audio_backend'] == 'winmm' and sys.platform == 'win32':
86
            self.bk: backend_base.BaseBackend = backend_winmm.WinMMBackend(self, ctypes.windll.winmm)
87
        else:
88
            raise FileNotFoundError('Unknown audio backend')
89
        if self.config['force_try_init']:
90
            for i in range(20):
91
                try:
92
                    self.bk.init()
93
                    break
94
                except RuntimeError as _err:
95
                    if i == 19:
96
                        raise _err
97
                    time.sleep(0.25)
98
                    continue
99
        else:
100
            self.bk.init()
101
        self.display_info()
102
        self.volume = self.config['volume']
103
        self.speed = self.config['speed']
104
        if self.volume > 1.0:
105
            raise RuntimeError(f'Volume {self.volume} is bigger than 1.0')
106
        self.full_list = []
107
        self.temp_list = []
108
        self.full_list_group = {}
109
        self.rescan()
110
        self.current_music: base_backend.BaseMusic = None # noqa
111
        self.running = True
112
        self.default_track_id = -1
113
        self.next_is_switch_to_main = False
114
        try:
115
            self.main_loop()
116
            self.should_kill = self.server.should_kill
117
        except KeyboardInterrupt:
118
            self.should_kill = self.server.should_kill
119
        self.cleanup()
120
        self.bk.quit()
121
        self.bk.destroy()
122
        self.exit_code = 0
123
        if self.should_kill:
124
            os.kill(os.getpid(), self.sig_kill)  # FIXME
125
    
126
    def rescan(self) -> None:
127
        self.full_list.clear()
128
        self.full_list_group.clear()
129
        for arg in self.argv:
130
            ext = arg.split('.')[-1].lower()
131
            if ext not in self.config['formats']:
132
                continue
133
            self.full_list.append(arg)
134
        if not self.full_list and self.config['music_path']:
135
            for fn in os.listdir(self.config['music_path']):
136
                ext = fn.split('.')[-1].lower()
137
                if ext not in self.config['formats']:
138
                    continue
139
                self.full_list.append(os.path.join(self.config['music_path'], fn))
140
        for track_fp in self.full_list:
141
            music_group = os.path.basename(track_fp).split(' - ')[0].strip()
142
            if music_group in self.full_list_group:
143
                self.full_list_group[music_group].append(track_fp)
144
            else:
145
                self.full_list_group[music_group] = [track_fp]
146
        if self.config['main_playlist_mode'] == 'random_pick':
147
            random.shuffle(self.full_list)
148
        log.info('Music scan results:', len(self.full_list), 'tracks in the full list')
149

150
    def track_loop(self) -> None:
151
        if self.config['print_json'] and self.config['print_json_time']:
152
            last_format = ''
153
            len_format = self.format_time(self.current_music.length)
154
        while self.running and self.current_music and self.current_music.is_playing():
155
            self.server.update()
156
            self.poll_commands()
157
            self.bk.update()
158
            if self.config['print_json'] and self.config['print_json_time']:
159
                cur_format = self.format_time(self.current_music.get_pos())
160
                if not cur_format == last_format:  # noqa
161
                    last_format = cur_format
162
                    output = {
163
                        'text': '[' + cur_format + '/' + len_format + '] ' +  # noqa
164
                                self.current_music.fn_no_ext,
165
                        'class': 'custom-mediaplayer',
166
                        'alt': 'mediaplayer'
167
                    }
168
                    sys.stdout.write(json.dumps(output) + '\n')
169
                    sys.stdout.flush()
170

171
    def next_track(self) -> any:
172
        # TODO: maybe allow to change mode in real time?
173
        if self.temp_list:
174
            fp = self.temp_list.pop(0)  # Only default and random pick modes currently
175
            if not self.temp_list:
176
                self.next_is_switch_to_main = True
177
            try:
178
                return self.bk.open_music(fp)
179
            except RuntimeError:
180
                return None
181
        if self.next_is_switch_to_main:
182
            self.next_is_switch_to_main = False
183
            log.info('Switched back to main list')
184
        if self.config['main_playlist_mode'] in ('default', 'random_pick'):
185
            self.default_track_id += 1
186
            if self.default_track_id >= len(self.full_list):
187
                if self.config['main_playlist_mode'] == 'random_pick':
188
                    random.shuffle(self.full_list)
189
                self.default_track_id = 0
190
            fp = self.full_list[self.default_track_id]
191
            try:
192
                return self.bk.open_music(fp)
193
            except RuntimeError:
194
                return None
195
        elif self.config['main_playlist_mode'] == 'random_full':
196
            fp = random.choice(self.full_list)
197
            try:
198
                return self.bk.open_music(fp)
199
            except RuntimeError:
200
                return None
201
        elif self.config['main_playlist_mode'] == 'random_group':
202
            group_tracks = random.choice(tuple(self.full_list_group.values()))
203
            fp = random.choice(group_tracks)
204
            try:
205
                return self.bk.open_music(fp)
206
            except RuntimeError:
207
                return None
208
        return None
209

210
    @staticmethod
211
    def format_time(need_time: float) -> str:
212
        round_time = round(need_time)
213
        sec_str = str(round(round_time) % 60)
214
        return str(int(round_time / 60)) + ':' + ('0' if len(sec_str) <= 1 else '') + sec_str
215

216
    def main_loop(self) -> None:
217
        pause_first = self.config['pause_first']
218
        while self.running:
219
            mus: backend_base.BaseMusic = self.next_track()
220
            while not mus:
221
                mus = self.next_track()
222
            self.play_new_music(mus)
223
            if pause_first:
224
                mus.set_paused(True)
225
                pause_first = False
226
            stat = os.stat(mus.fp)
227
            info = mus.fn_no_ext
228
            if mus.length:
229
                info += f' [{self.format_time(mus.length)}]'
230
            if mus.freq:
231
                info += f' [{int(mus.freq)}Hz]'
232
            if not mus.type == 'none':
233
                info += f' [{mus.type.upper()}]'
234
            if stat.st_mtime:
235
                info += f' [{str(datetime.datetime.fromtimestamp(int(stat.st_mtime)))}]'
236
            log.info(info)
237
            if self.config['print_json'] and not self.config['print_json_time']:
238
                output = {
239
                    'text': mus.fn_no_ext,
240
                    'class': 'custom-mediaplayer',
241
                    'alt': 'mediaplayer'
242
                }
243
                sys.stdout.write(json.dumps(output) + '\n')
244
                sys.stdout.flush()
245
            self.track_loop()
246

247
    def play_new_music(self, mus: backend_base.BaseMusic) -> None:
248
        if self.current_music:
249
            self.current_music.stop()
250
            self.current_music.destroy()
251
        mus.play()
252
        mus.set_volume(self.volume)
253
        mus.set_speed(self.speed)
254
        self.current_music = mus
255

256
    def poll_commands(self) -> None:
257
        temp_mus = []
258
        while self.server.commands:
259
            cmds = self.server.commands.pop(0)
260
            for _cmd in cmds.split(';'):
261
                cmd = _cmd.strip()
262
                if os.path.isfile(cmd) and cmd.split('.')[-1].lower() in self.config['formats']:
263
                    temp_mus.append(cmd)
264
                    continue
265
                if cmd == 'next':
266
                    if self.current_music:
267
                        self.current_music.stop()
268
                elif cmd in ('toggle_pause', 'pause', 'resume'):
269
                    if self.current_music:
270
                        if cmd == 'toggle_pause':
271
                            paused = not self.current_music.paused
272
                        else:
273
                            paused = cmd == 'pause'
274
                        self.current_music.set_paused(paused)
275
                        log.info('Paused:', self.current_music.paused)
276
                elif cmd == '--client-only' or cmd == '--server-only':
277
                    pass
278
                elif cmd == 'clear_temp':
279
                    self.temp_list.clear()
280
                    if self.current_music:
281
                        self.current_music.stop()
282
                    # log.info('Temp music list cleared')
283
                elif cmd == 'show_pos':
284
                    if self.current_music:
285
                        log.info(f'Music Position: {self.format_time(self.current_music.get_pos())}')
286
                elif cmd == 'rewind':
287
                    if self.current_music:
288
                        self.current_music.rewind()
289
                elif cmd.startswith('pos_sec'):
290
                    if not self.current_music:
291
                        continue
292
                    try:
293
                        new_pos = float(cmd.split(' ')[-1])
294
                    except (ValueError, IndexError) as _err:
295
                        log.warn(f'Could not convert position value:', _err)
296
                        continue
297
                    if cmd.startswith('pos_sec '):
298
                        cur_pos = 0.0
299
                    else:
300
                        cur_pos = self.current_music.get_pos()
301
                    need_pos = max(min(cur_pos + new_pos, 60.0 * 60.0 * 100.0), 0.0)
302
                    self.current_music.set_pos(need_pos)
303
                    got_pos = self.current_music.get_pos()
304
                    log.info('New Position:', self.format_time(got_pos))
305
                elif cmd.startswith('pos_rel'):
306
                    if not self.current_music or not self.current_music.length:
307
                        continue
308
                    try:
309
                        new_pos = float(cmd.split(' ')[-1])
310
                    except (ValueError, IndexError) as _err:
311
                        log.warn(f'Could not convert position value:', _err)
312
                        continue
313
                    if cmd.startswith('pos_rel '):
314
                        cur_pos = 0.0
315
                    else:
316
                        cur_pos = self.current_music.get_pos() / self.current_music.length
317
                    need_pos = max(min(cur_pos + new_pos, 100.0), 0.0)
318
                    self.current_music.set_pos(need_pos * self.current_music.length)
319
                    got_pos = self.current_music.get_pos()
320
                    log.info('New Position:', self.format_time(got_pos))
321
                elif cmd.startswith('volume'):
322
                    try:
323
                        new_volume = float(cmd.split(' ')[-1])
324
                    except (ValueError, IndexError) as _err:
325
                        log.warn(f'Could not convert volume value:', _err)
326
                        continue
327
                    if cmd.startswith('volume '):
328
                        self.volume = 0.0
329
                    self.volume = max(min(self.volume + new_volume, 1.0), 0.0)
330
                    if self.current_music:
331
                        self.current_music.set_volume(self.volume)
332
                    log.info('New Volume:', self.volume)
333
                elif cmd.startswith('speed'):
334
                    try:
335
                        new_speed = float(cmd.split(' ')[-1])
336
                    except (ValueError, IndexError) as _err:
337
                        log.warn(f'Could not convert speed value:', _err)
338
                        continue
339
                    if cmd.startswith('speed '):
340
                        self.speed = 0.0
341
                    self.speed = max(min(self.speed + new_speed, 1000.0), -1000.0)
342
                    if self.current_music:
343
                        self.current_music.set_speed(self.speed)
344
                    log.info('New Speed:', self.speed)
345
                elif cmd == 'rescan':
346
                    self.rescan()
347
                elif cmd == 'exit' or cmd == 'quit':
348
                    self.running = False
349
                else:
350
                    log.warn('Unknown Command', cmd)
351
        if temp_mus:
352
            self.temp_list = temp_mus
353
            self.temp_list_prepare()
354
            log.info('Playing Temp Playlist')
355
            if self.current_music:
356
                self.current_music.stop()
357

358
    def temp_list_prepare(self) -> None:
359
        if self.config['main_playlist_mode'] == 'default' and self.temp_list[-1] in self.full_list:
360
            self.default_track_id = self.full_list.index(self.temp_list[-1]) + 1
361
        if self.config['temp_playlist_mode'] == 'random_pick':  # Trick
362
            random.shuffle(self.temp_list)
363

364
    def display_info(self) -> None:
365
        log.info('Welcome to the Pixelsuft MUST!')
366
        log.info('Found Drivers:', ', '.join(self.bk.get_audio_drivers()))
367
        log.info('Current Driver:', self.bk.get_current_audio_driver())
368
        log.info('Found Devices:', ', '.join(self.bk.get_audio_devices_names()))
369
        log.info('Current Device:', self.bk.get_current_audio_device_name())
370

371
    def cleanup(self) -> None:
372
        if self.server:
373
            self.server.destroy()
374
            # self.server = None
375
        if self.current_music:
376
            self.current_music.stop()
377
            self.current_music.destroy()
378
            self.current_music = None
379

380
    def client_prompt(self) -> None:
381
        msg = 'i_want_to_live_please_do\'nt_die'
382
        while msg is not None:
383
            try:
384
                self.client.send(msg)
385
                if msg == 'disconnect' or msg == 'exit' or msg == 'quit':
386
                    self.exit_code = 0
387
                    return
388
            except RuntimeError:
389
                return
390
            msg = input('>>> ')
391

392
    def read_json(self, fp: str) -> dict:
393
        f = open(fp, 'r', encoding=self.encoding)
394
        content = f.read()
395
        f.close()
396
        return json.loads(content)
397

398
    def write_json(self, fp: str, content: dict) -> int:
399
        f = open(fp, 'w', encoding=self.encoding)
400
        result = f.write(json.dumps(content, indent=4))
401
        f.close()
402
        return result
403

404
    def stb(self, str_to_encode: str, encoding=None) -> bytes:
405
        return str_to_encode.encode(encoding or self.encoding, errors='replace')
406

407
    def bts(self, bytes_to_decode: bytes, encoding=None) -> str:
408
        return bytes_to_decode.decode(encoding or self.encoding, errors='replace')
409

410
    def search_libs(self, *libs: any, prefix: str = '') -> dict:
411
        result = {}
412
        for path in self.paths:
413
            for lib in libs:
414
                if result.get(lib):
415
                    continue
416
                try:
417
                    result[lib] = self.load_library(
418
                        os.path.join(path, prefix + lib + self.auto_postfix)
419
                    )
420
                except (FileNotFoundError, OSError):
421
                    continue
422
        for lib in libs:
423
            if result.get(lib):
424
                continue
425
            try:
426
                result[lib] = self.load_library(prefix + lib + self.auto_postfix)
427
            except (FileNotFoundError, OSError):
428
                continue
429
        return result
430

431

432
if __name__ == '__main__':
433
    sys.exit(App(sys.argv).exit_code)
434

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.