forked from Vivojay/mariana-music-player
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
2992 lines (2503 loc) · 138 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#################################################################################################################################
#
# Mariana Player v0.6.2 dev
# (Read help.md for help on commands)
#
# Running the app:
# For very first boot (SETUP):
# Make sure you have python version < 3.10 to run this file (unless compatible llvmlite wheel bins exist...)
#
# QUICK-SETUP (NEW DROP-IN REPLACEMENT FOR MANUAL SETUP!)
# Run INITSETUP.py and follow along with it's instructions (Run with "--help" flag for more info)
# *NOTE: Don't run manual setup if you have already done a quick setup
# *BENEFITS: Enjoy auto created ".bat" and ".ps1" runner files to automate successive runs of Mariana Player
# |
# +-----<--(You can skip to here after the QUICK-SETUP)---------<-+
# |
# | MANUAL SETUP (Go through a tedious setup procedure)
# | Setup compatible architecture of VLC media player, install FFMPEG and add to path...
# | Install git scm if not already installed
# v Install given git package directly from url using: `pip install git+https://github.com/Vivojay/pafy@develop`
# | run `pip install -r requirements.txt`
# |
# v *OPTIONAL: Download and pip install unofficial binary for llvmlite wheel compatible with your python version
# | *NOTE: Specify py version < 3.10 in virtualenv (if installing optional llvmlite), as other py vers don't support llvmlite wheels :)
# |
# +---> Firstly, look at help.md before running any py file
# Run this file (main.py) on the very first bootup, nothing else (no flags, just to test bare minimum run)...
# You are good to go...
# *Note: If you encounter errors, look for online help as the current help file doesn't have fixes for common problems yet
#
# All successive boots (RUNNING NORMALLY):
# just run this file (main.py) with desired flags (discussed in help.md)
# and enjoy... (and possibly debug...)
# This app may take a LOT of time to load at first...
# Hence the loading prompt...
# Editor's Note: Make sure to brew a nice coffee beforehand... :)
#################################################################################################################################
# IMPORTS BEGIN #
import time
APP_BOOT_START_TIME = time.time(); print("Loaded 1/31", end='\r')
import os; print("Loaded 2/31", end='\r')
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
# import itertools; print("Loaded 3/31", end='\r')
import re; print("Loaded 3/31", end='\r')
import sys; print("Loaded 4/31", end='\r')
import pygame; print("Loaded 5/31", end='\r')
import numpy as np; print("Loaded 6/31", end='\r')
import random as rand; print("Loaded 7/31", end='\r')
import importlib; print("Loaded 8/31", end='\r')
import colored; print("Loaded 9/31", end='\r')
import subprocess as sp; print("Loaded 10/31", end='\r')
import restore_default; print("Loaded 11/31", end='\r')
import toml; print("Loaded 12/31", end='\r')
import json; print("Loaded 13/31", end='\r')
import webbrowser; print("Loaded 14/31", end='\r')
# import concurrent.futures; print("Loaded 15/31", end='\r')
import sounddevice; print("Loaded 15/31", end='\r')
# from scipy.io.wavfile import read; print("Loaded 15/31", end='\r')
from getpass import getpass; print("Loaded 16/31", end='\r')
from url_validate import url_is_valid; print("Loaded 17/31", end='\r')
from tabulate import tabulate as tbl; print("Loaded 18/31", end='\r')
from ruamel.yaml import YAML; print("Loaded 19/31", end='\r')
from collections.abc import Iterable; print("Loaded 20/31", end='\r')
from logger import SAY; print("Loaded 21/31", end='\r')
from multiprocessing import Process; print("Loaded 22/31", end='\r')
from first_boot_welcome_screen import notify; print("Loaded 23/31", end='\r')
online_streaming_ext_load_error = 0
comtypes_load_error = False # Made available after fix from comtypes issue #244, #180
# Previously: comtypes_load_error = True
lyrics_ext_load_error = 0
reddit_creds_are_valid = False
# try:
# import librosa
# print("Loaded 23/31", end='\r') # Time taking import (Sometimes, takes ages...)
# except ImportError:
# print("[WARN] Could not load music computation extension...")
# print("[WARN] ...Skipped 23/31")
CURDIR = os.path.dirname(os.path.realpath(__file__))
os.chdir(CURDIR)
try:
vas = importlib.import_module("beta.vlc-async-stream")
print("Loaded 24/31", end='\r')
except ImportError:
online_streaming_ext_load_error = 1
print("[INFO] Could not load online streaming extension...")
print("[INFO] ...Skipped 24/31")
try:
YT_query = importlib.import_module("beta.YT_query")
print("Loaded 25/31", end='\r')
except ImportError:
# raise
if not online_streaming_ext_load_error:
print("[INFO] Could not load online streaming extension...")
print("[INFO] ...Skipped 25/31")
try:
from beta.IPrint import IPrint, blue_gradient_print, loading, cols
print("Loaded 26/31", end='\r')
except ImportError:
lyrics_ext_load_error = 1
print("[INFO] Could not load coloured print extension...")
print("[INFO] ...Skipped 26/31")
try:
os.chdir(CURDIR)
from lyrics_provider import get_lyrics
print("Loaded 27/31", end='\r')
except ImportError:
print("[INFO] Could not load lyrics extension...")
if not lyrics_ext_load_error:
print("[INFO] ...Could not load online streaming extension...")
print("[INFO] ...Skipped 27/31")
try:
from beta import redditsessions
if redditsessions.WARNING:
print("[WARN] Could not load reddit-sessions extension...")
print(f"[WARN] ...{redditsessions.WARNING}...")
print("[WARN] ...Skipped 28/31")
else:
reddit_creds_are_valid = True
print("Loaded 28/31", end='\r')
except ImportError:
print("[INFO] Could not load reddit-sessions extension..., module 'praw' missing...")
print("[INFO] ...Skipped 28/31")
try:
from lyrics_provider.detect_song import get_song_info
print("Loaded 29/31", end='\r')
except ImportError:
print("[INFO] Could not load lyrics extension...")
if not lyrics_ext_load_error:
print("[INFO] ...Could not load online streaming extension...")
print("[INFO] ...Skipped 29/31")
try:
from beta.master_volume_control import get_master_volume, set_master_volume
print("Loaded 30/31", end='\r')
except Exception:
comtypes_load_error = True
SAY(visible=False, # global var `visible` hasn't been defined yet...
log_message="comtypes load failed",
display_message="", # ...because we don't want to display anything on screen to the user
log_priority=2)
try:
from beta.podcasts import get_latest_podbean_data, vendors as pod_vendors
print("Loaded 31/31", end='\r')
except Exception:
try:
from lyrics_provider.detect_song import get_song_info
print("Loaded 31/31", end='\r')
except ImportError:
print("[INFO] Could not load lyrics extension...")
if not lyrics_ext_load_error:
print("[INFO] ...Could not load online streaming extension...")
print("[INFO] ...Skipped 31/31")
os.chdir(CURDIR)
# IMPORTS END #
# TODO - Replace `err`s with `SAY`s
# TODO - Rename SAY to `err` or `errlogger`... in whole codebase?
# TODO - Add option to display type of error in display_message parameter of `SAY`
# to print kind of log [ (debg)/(info)/(warn)/(fatl) ] ??
yaml = YAML(typ='safe') # Allows for safe YAML loading
webbrowser.register_standard_browsers()
if not os.path.isdir('logs'): os.mkdir('logs')
def create_required_files_if_not_exist(*files):
for file in files:
if not os.path.isfile(file):
with open(file, 'w', encoding="utf-8") as _:
pass
create_required_files_if_not_exist(
'logs/history.log',
'logs/general.log',
)
FIRST_BOOT = False # Assume user is using app for considerable time
# so you don't want to annoy him with an
# annoying FIRST-TIME-WELCOME
try:
with open('settings/system.toml', encoding='utf-8') as file:
SYSTEM_SETTINGS = toml.load(file)
FIRST_BOOT = SYSTEM_SETTINGS['first_boot']
except IOError:
SYSTEM_SETTINGS = None
ISDEV = SYSTEM_SETTINGS['isdev'] # Useful as a test flag for new features
try:
with open('lib.lib', encoding='utf-8') as logfile:
paths = logfile.read().splitlines()
paths = [path for path in paths if not path.startswith('#')]
paths = list(set(paths))
except IOError:
if not FIRST_BOOT:
sys.exit("[INFO] Could not find lib.lib file, '\
'please create one and add desired source directories. '\
'Aborting program\n")
def first_startup_greet(is_first_boot):
global SOFT_FATAL_ERROR_INFO
if is_first_boot:
try:
import first_boot_setup
if SOFT_FATAL_ERROR_INFO := first_boot_setup.fbs(
about=SYSTEM_SETTINGS
):
SOFT_FATAL_ERROR_INFO = "User skipped startup"
reload_sounds(quick_load = False)
except ImportError:
sys.exit('[ERROR] Critical guide setup-file missing, please consider reinstalling this file or the entire program\nAborting Mariana Player. . .')
try:
with open('user/user_data.yml', encoding='utf-8') as u_data_file:
USER_DATA = yaml.load(u_data_file)
if (
list(USER_DATA.keys()) == ['default_user_data']
and not FIRST_BOOT
and not ISDEV
):
SAY(visible=visible,
display_message = '',
log_message = 'User data found to be empty, reverting to default',
log_priority = 3)
except IOError:
SAY(visible=visible,
display_message = f'Encountered missing program file @{os.path.join(CURDIR, "user/user_data.yml")}',
log_message = 'User data file not found',
log_priority = 1) # Log fatal crash
sys.exit(1) # Fatal crash
try:
with open('settings/settings.yml', encoding='utf-8') as u_data_file:
SETTINGS = yaml.load(u_data_file)
except IOError:
SAY(visible=visible,
display_message = f'Encountered missing program file @{os.path.join(CURDIR, "settings/settings.yml")}',
log_message = 'Aborting player because settings file was not found',
log_priority = 1) # Log fatal crash
sys.exit(1) # Fatal crash
# Variables
APP_BOOT_END_TIME = time.time()
EXIT_INFO = 0
FATAL_ERROR_INFO = None
SOFT_FATAL_ERROR_INFO = None
RECENTS_QUEUE = []
YOUTUBE_PLAY_TYPE = None
isplaying = False
currentsong = None # No audio playing initially
ismuted = False
lyrics_saved_for_song = False
currentsong_length = None
songindex = -1
lyrics_window_note = "[Please close the lyrics window to continue issuing more commands...]"
current_media_type = None
"""
NO SUCH THING AS current_media_player now
random audio (optional repeat, no repeat by default)
log data about each audio path
timestamp of play (int --> in unix time??)
list of tags (str List)
favourited? (bool)
corrupted? (bool)
blacklisted? (bool)
duration played (int --> in ms)
# Maybe
estimated bpm
estimated key/scale
"""
# Log levels from logger.py -> [Only for REF]
# logleveltypes = {0: "none", 1: "fatal", 2: "warn", 3: "info", 4: "debug"}
# From settings
disable_OS_requirement = SYSTEM_SETTINGS['system_settings']['enforce_os_requirement']
# Supported file extensions
# (For *.wav get_pos() in pygame provides played duration and not actual play position)
supported_file_types = SYSTEM_SETTINGS["system_settings"]['supported_file_types']
max_wait_limit_to_get_song_length = SYSTEM_SETTINGS['system_settings']['max_wait_limit_to_get_song_length']
MAX_RECENTS_SIZE = SYSTEM_SETTINGS["system_settings"]['max_recents_size']
visible = SETTINGS['visible']
loglevel = SETTINGS.get('loglevel')
DEFAULT_EDITOR = SETTINGS.get('editor path')
FALLBACK_RESULT_COUNT = SETTINGS['display items count']['general']['fallback']
MAX_RESULT_COUNT = SETTINGS['display items count']['general']['maximum']
max_yt_search_results_threshold = SETTINGS['display items count']['youtube-search results']['maximum']
if not loglevel:
restore_default.restore('loglevel', SETTINGS)
loglevel = SETTINGS.get('loglevel')
# From last session info
cached_volume = 1 # Set as a factor between 0 to 1 times of max volume player volume
def OrderedSet(iterable):
result = []
[result.append(i) for i in iterable if i not in result]
return result
# Flattens list of any depth
def flatten(l):
for el in l:
if isinstance(el, Iterable) and not isinstance(el, (str, bytes)):
yield from flatten(el)
else:
yield el
# Function to extract files from folders recursively
def audio_file_gen(Dir, ext):
for root, dirs, files in os.walk(Dir):
for filename in files:
if os.path.splitext(filename)[1] == ext:
yield os.path.join(root, filename)
def reload_sounds(quick_load = True):
global _sound_files, _sound_files_names_only, _sound_files_names_enumerated, paths
# NOTE: 'data/snd_files.json' is the relpath to the quick-loads file
lib_found = True
# Definition for quick-load
if quick_load:
if os.path.isfile('data/snd_files.json'):
with open('data/snd_files.json', encoding="utf-8") as fp:
_sound_files = json.load(fp)
else: # Revert to full load (i.e. NOT resorting to quick_load becuase data/snd_files.json is unavailable)
quick_load = False
# Definition for full-load (non quick-load)
if not quick_load: # (This may be used either as the first-time load or as a fallback for a failed quick-load)
if os.path.isfile('lib.lib'):
with open('lib.lib', encoding='utf-8') as logfile:
paths = logfile.read().splitlines()
paths = [path for path in paths if not path.startswith('#')]
from beta import mediadl
dl_dir_setup_code = mediadl.setup_dl_dir(SETTINGS, SYSTEM_SETTINGS)
if dl_dir_setup_code not in range(4):
dl_dir = dl_dir_setup_code
if sys.platform == 'win32': dl_dir=dl_dir.replace('/', '\\')
else: dl_dir=dl_dir.replace('\\', '/')
paths.append(dl_dir)
else:
# ERRORS have already been handled and logged by `mediadl.setup_dl_dir()`
pass
paths = list(OrderedSet(paths))
# Use the recursive extractor function and format and store them into usable lists
_sound_files = [[list(audio_file_gen(paths[j], supported_file_types[i]))
for i in range(len(supported_file_types))] for j in range(len(paths))]
# Flattening irregularly nested sound files
_sound_files = list(flatten(_sound_files))
with open('data/snd_files.json', 'w', encoding='utf-8') as fp:
json.dump(_sound_files, fp)
else:
lib_found = False
SAY(visible=visible,
log_message="Library file suddenly made unavailable",
display_message="Library file suddenly vanished -_-",
log_priority=2)
if lib_found:
_sound_files_names_only = [os.path.splitext(os.path.split(i)[1])[0] for i in _sound_files]
_sound_files_names_enumerated = [(i+1, j) for i, j in enumerate(_sound_files_names_only)]
if FIRST_BOOT:
with open('data/snd_files.json', 'w', encoding='utf-8') as fp:
json.dump(_sound_files, fp)
reload_sounds(quick_load = not FIRST_BOOT) # First boot requires quick_load to be disabled,
# other boots can do away with quick_loads :)
if _sound_files_names_only == []:
if loglevel in [3, 4]:
IPrint("[INFO] All source directories are empty, you may and add more source directories to your library", visible=visible)
IPrint("[INFO] To edit this library file (of source directories), refer to the `help.md` markdown file.", visible=visible)
try: _ = sp.run('ffmpeg', stdout=sp.DEVNULL, stdin=sp.PIPE, stderr=sp.DEVNULL)
except FileNotFoundError: FATAL_ERROR_INFO = "ffmpeg not recognised globally, download it and add to path (system environment)"
try: _ = sp.run('ffprobe', stdout=sp.DEVNULL, stdin=sp.PIPE, stderr=sp.DEVNULL)
except FileNotFoundError: FATAL_ERROR_INFO = "ffprobe not recognised globally, download it and add to path (system environment)"
if reddit_creds_are_valid: r_seshs = redditsessions.get_redditsessions()
else: r_seshs = None
def recents_queue_save(inf):
"""
inf = {
'yt_play_type': int,
'type': int,
'identity': (song_info_as_tuple) OR 'some/absolute/file/path',
}
inf is a dict of "yt_play_type" (applicable only for YT streams), "identity" and "type" of audio
"identity" is a kind of unique locater for a audio. It can be a streaming url
or the filepath of a locally streamed audio (as a string)
Songs are pushed to the RECENTS_QUEUE and when it is full
the oldest audios are removed first to clear space for the new ones
RECENTS_QUEUE has a fixed size (determined by settings.yml)
(max allowed value = 10,000,000 (1 Million) items)
"""
global RECENTS_QUEUE, MAX_RECENTS_SIZE
if current_media_type is None: # Playing local
inf = [None, -1, inf]
else:
if current_media_type == 0:
inf = [YOUTUBE_PLAY_TYPE, current_media_type, inf]
else:
inf = [None, current_media_type, inf]
# Clear atleast 1 space for the new item
if len(RECENTS_QUEUE) == MAX_RECENTS_SIZE:
del RECENTS_QUEUE[0]
# Store item in the newly cleared space
RECENTS_QUEUE.append(inf)
def open_in_youtube(local_song_file_path):
if local_song_detected_name := get_song_info(
local_song_file_path, get_title_only=True
):
_, youtube_search_query_url = YT_query.search_youtube(search=local_song_detected_name)
webbrowser.open(youtube_search_query_url)
return 0
else:
SAY(visible=visible,
display_message = 'Could not detect the current audio',
log_message = 'Could not detect the current audio',
log_priority = 3)
return 1
def get_current_progress():
return (vas.vlc_media_player.get_media_player().get_time() / 1000)
def save_user_data():
global USER_DATA
total_plays = [j for i, j in
USER_DATA['default_user_data']['stats']['play_count'].items()
if i in ['local', 'radio', 'general', 'youtube', 'redditsession']]
total_plays = sum(total_plays)
USER_DATA['default_user_data']['stats']['play_count']['total'] = total_plays
with open('user/user_data.yml', 'w', encoding="utf-8") as u_data_file:
yaml.dump(USER_DATA, u_data_file)
def save_song_data():
global currentsong_length, currentsong
SONG_DATA = []
with open('data/song_data.json', 'w', encoding="utf-8") as s_data_file:
json.dump(SONG_DATA, s_data_file)
def exitplayer(sys_exit=False):
global EXIT_INFO, APP_BOOT_START_TIME, USER_DATA
stopsong()
APP_CLOSE_TIME = time.time()
time_spent_on_app = APP_CLOSE_TIME - APP_BOOT_END_TIME
app_boot_time = APP_BOOT_END_TIME - APP_BOOT_START_TIME
SAY(visible=visible,
display_message = '',
log_message = f'Time spent to boot app = {app_boot_time}',
log_priority = 3)
SAY(visible=visible,
display_message = '',
log_message = f'Time spent using app = {time_spent_on_app}',
log_priority = 3)
USER_DATA['default_user_data']['stats']['times_spent'].append(time_spent_on_app)
save_user_data()
IPrint(colored.fg('red')+'Exiting...'+colored.attr('reset'), visible=visible)
if sys_exit:
sys.exit(f"{EXIT_INFO}")
# def loadsettings():
# global settings
# with open('', encoding='utf-8') as settingsfile:
# settings = yaml.load(settingsfile)
def play_local_default_player(songpath, _songindex, is_queue=False):
global isplaying, currentsong, currentsong_length, songindex
global USER_DATA, current_media_type, SONG_CHANGED
try:
vas.set_media(_type='local', localpath=songpath)
vas.media_player(action='play')
vas.vlc_media_player.get_media_player().audio_set_volume(int(cached_volume*100))
isplaying = True
currentsong = songpath
if _songindex:
IPrint(colored.fg('dark_olive_green_2') + \
f':: {_sound_files_names_only[int(_songindex)-1]}' + \
colored.attr('reset'), visible=visible)
# The user is unreliable and may enter the
# audio path with weird inhumanly erratic and random
# mix of upper and lower case characters.
# Hence, we need to convert everything to lowercase...
try:
songindex = [i.lower() for i in _sound_files].index(songpath.lower())+1
except:
songindex = 'N/A'
recents_queue_save((songindex, currentsong))
else:
if is_queue:
IPrint(colored.fg('dark_olive_green_2') + '::queue' + \
colored.attr('reset'), visible=visible)
else:
IPrint(colored.fg('dark_olive_green_2') + \
f':: {os.path.splitext(os.path.split(songpath)[1])[0]}' + \
colored.attr('reset'), visible=visible)
recents_queue_save(currentsong)
current_media_type = None
USER_DATA['default_user_data']['stats']['play_count']['local'] += 1
save_user_data()
while not vas.vlc_media_player.get_media_player().is_playing(): pass
# TODO - Save all audio info in `data` dir
# save_song_data()
IPrint(f"{colored.fg('grey_50')}Attempting to calculate audio length{colored.fg('grey_50')}", visible=visible)
length_find_start_time = time.time()
while True:
if vas.vlc_media_player.get_media_player().get_length():
currentsong_length = vas.vlc_media_player.get_media_player().get_length()/1000
break
if time.time() - length_find_start_time >= max_wait_limit_to_get_song_length:
currentsong_length = -1
break
if currentsong_length == -1:
SAY(visible=visible,
log_message = "Cannot get length for vas media",
display_message = "",
log_priority=3)
isplaying = True
if not currentsong_length and currentsong_length != -1:
get_currentsong_length()
# Save current audio to log/history.log in human readable form
SAY(visible=visible,
display_message = '',
out_file='logs/history.log',
log_message = currentsong,
log_priority = 3,
format_style = 0)
except Exception:
#raise
SAY(visible=visible,
display_message=f"Failed to play \"{songpath}\"",
log_message=f"Failed to play audio: \"{songpath}\"",
log_priority=2,)
def voltransition(
initial=cached_volume,
final=cached_volume,
disablecaching=False, # NOT_USED: Enable volume caching by default
transition_time=0.2,
show_progress = False,
# transition_time=1,
):
global cached_volume, visible
if not ismuted:
for i in range(101):
diffvolume = initial*100+(final-initial)*i
time.sleep(transition_time/100)
if show_progress and visible:
print(f'{colored.fg("orange_1")} -> {i}%', end='\r')
print(colored.attr('reset'), end = '\r')
vas.vlc_media_player.get_media_player().audio_set_volume(int(diffvolume))
# if not disablecaching:
# cached_volume = final
def vol_trans_process_spawn():
vol_trans_process = Process(target=voltransition,
args=({'initial': cached_volume,
'final': 0,
'disablecaching': True}))
vol_trans_process.start()
vol_trans_process.join()
# https://stackoverflow.com/a/3463582/17685480
def remove_adjacent(seq): # works on any sequence, not just on numbers
i = 1
n = len(seq)
while i < n: # avoid calling len(seq) each time around
if seq[i] == seq[i-1]:
del seq[i]
# value returned by seq.pop(i) is ignored; slower than del seq[i]
n -= 1
else:
i += 1
#### return seq #### don't do this
# function acts in situ; should follow convention and return None
def playpausetoggle(softtoggle=True, use_multi=False, transition_time=0.2, show_progress=False): # Soft pause by default
global isplaying, currentsong, cached_volume
try:
if currentsong:
if isplaying:
# with concurrent.futures.ProcessPoolExecutor() as executor:
if use_multi:
vol_trans_process_spawn()
else:
voltransition(initial=cached_volume,
final=0,
transition_time=transition_time*(softtoggle),
disablecaching=True,
show_progress=show_progress)
# executor.submit(voltransition,
# initial=cached_volume,
# transition_time=transition_time*(softtoggle),
# final=0,
# disablecaching=True)
vas.media_player(action='pausetoggle')
if visible: print(' '*12, end='\r')
IPrint("|| Paused", visible=visible)
isplaying = False
else:
vas.media_player(action='pausetoggle')
# with concurrent.futures.ProcessPoolExecutor() as executor:
vas.vlc_media_player.get_media_player().audio_set_volume(0)
if use_multi:
vol_trans_process_spawn()
else:
voltransition(initial=0,
final=cached_volume,
transition_time=transition_time*(softtoggle),
disablecaching=True,
show_progress=show_progress)
# executor.submit(voltransition, initial=0, final=cached_volume)
if visible: print(' '*12, end='\r')
IPrint("|> Resumed", visible=visible)
isplaying = True
else:
isplaying = False
SAY("Nothing to pause/unpause", say=False)
except Exception:
# raise
SAY(
visible=visible,
log_priority=2,
display_message=f"Failed to toggle play/pause for \"{currentsong}\"",
log_message=f"Failed to toggle play pause for audio: \"{currentsong}\"",
)
def stopsong():
global isplaying, currentsong
try:
vas.media_player(action='stop')
currentsong = None
isplaying = False
purge_old_lyrics_if_exist()
except Exception:
IPrint(f'Failed to stop: {currentsong}', visible=visible)
def searchsongs(queryitems):
global _sound_files_names_enumerated
out = []
for index, audio in _sound_files_names_enumerated:
flag = True
for queryitem in list(OrderedSet(queryitems)):
if queryitem.lower() not in audio.lower():
flag = False
if flag:
out.append((index, audio))
return out
# TODO - Implement librosa bpm + online bpm API features
# def get_bpm(filename, duration=50, enable_round=True):
# y, sr = librosa.load(filename, duration=duration)
# tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
# if enable_round:
# return round(tempo)
# else:
# return tempo
def fade_in_out(initvol=None, finalvol=None, fade_type=0, fade_duration=5):
"""
To fade out, use fade_type = 1
To fade in, use fade_type = 0
"""
global cached_volume, isplaying, ismuted
if initvol is None: initvol = cached_volume
if ismuted:
SAY(visible=visible,
display_message='Cannot fade audio in or out when muted',
log_message='Cannot fade audio in or out when muted',
log_priority=3)
if finalvol is not None: # Complex fade has been command issued,
# execute it
if finalvol != 0: # Music is paused, resume and then fade in to v > 0
# Resume music
if not isplaying:
IPrint("|> Resumed", visible=visible)
vas.media_player(action='pausetoggle')
isplaying = True
voltransition(initial=initvol,
final=finalvol,
transition_time=fade_duration,
disablecaching=True,
show_progress=True)
if finalvol != 0: cached_volume = finalvol
if visible: print(' '*12, end='\r')
if finalvol == 0:
if isplaying:
vas.media_player(action='pausetoggle')
IPrint("|> Paused", visible=visible)
isplaying = False
elif fade_type == 0 and isplaying:
SAY(visible=visible,
display_message='Cannot fade in, audio already playing. Try pausing',
log_message='Cannot fade in, audio already playing. Try pausing',
log_priority=3)
elif fade_type == 0 or isplaying:
playpausetoggle(softtoggle = True, transition_time=fade_duration, show_progress=True)
else:
SAY(visible=visible,
display_message='Cannot fade out, no audio playing',
log_message='Cannot fade out, no audio playing',
log_priority=3)
def enqueue(songindices):
print(f'Enqueueing feature is still in progress... The developer {colored.fg("magenta_3a")}@{SYSTEM_SETTINGS["about"]["author"]}{colored.attr("reset")} will add this feature shortly...')
'''
# TODO - Refine the following feature and add to production
IPrint("Enqueueing", visible=visible)
global song_paths_to_enqueue
song_paths_to_enqueue = []
for songindex in songindices:
if int(songindex)-1 in range(len(_sound_files)):
song_paths_to_enqueue.append(_sound_files[int(songindex)-1])
else:
IPrint(f"Skipping index: {int(songindex)-1}", visible=visible)
if song_paths_to_enqueue:
song_paths_to_enqueue = list(OrderedSet(song_paths_to_enqueue))
for songpath in song_paths_to_enqueue:
try:
IPrint(f"Queued {song_paths_to_enqueue.index(songpath)+1}", visible=visible)
except Exception:
SAY(visible=visible,
display_message = "Queueing error",
log_message = "Could not enqueue one or more files",
log_pripority = 2)
raise
play_local_default_player(song_paths_to_enqueue, _songindex=None, is_queue=True)
else:
IPrint("No songs to queue", visible=visible)
'''
def purge_old_lyrics_if_exist():
lyrics_file_paths = ['temp/lyrics.txt', 'temp/lyrics.html']
for lyrics_file_path in lyrics_file_paths:
try:
if os.path.isfile(lyrics_file_path):
os.remove(lyrics_file_path)
except Exception:
raise
def local_play_commands(commandslist, _command=False):
global cached_volume, currentsong_length, lyrics_saved_for_song
# pygame.mixer.music.set_volume(cached_volume)
purge_old_lyrics_if_exist()
lyrics_saved_for_song = None
if not _command:
if len(commandslist) == 2:
songindex = commandslist[1]
if songindex.isnumeric():
if int(songindex) in range(1, len(_sound_files)+1):
currentsong_length = None
play_local_default_player(songpath = _sound_files[int(songindex)-1],
_songindex = songindex)
else:
if any(_sound_files):
SAY(visible=visible,
log_message='Out of bound audio index',
display_message=f'Song number {songindex} does not exist. Please input audio number between 1 and {len(_sound_files)}',
log_priority=3)
else:
SAY(visible=visible,
log_message='User attempted to play local audio, even though there are no audios in library',
display_message='There are no audios in library',
log_priority=2)
else:
# TODO - Implement full queue functionality
# as per `future ideas{...}.md`
# Not yet implemented
# This is just a sekeleton code for future
songindices = commandslist[1:]
_ = []
for songindex in songindices:
try:
if songindex.isnumeric():
_.append(songindex)
except Exception:
pass
songindices = _
del _
enqueue(songindices)
else:
currentsong_length = None
play_local_default_player(songpath=_command[1:], _songindex=None)
def timeinput_to_timeobj(rawtime):
try:
if ':' in rawtime.strip():
processed_rawtime = rawtime.split(':')
processed_rawtime = [int(i) if i else 0 for i in processed_rawtime]
# print(processed_rawtime)
# timeobj: A list of the format [WHOLE HOURS IN SECONDS, WHOLE MINUTES in SECONDS, REMAINING SECONDS]
timeobj = [value * 60 ** (len(processed_rawtime) - _index - 1)
for _index, value in enumerate(processed_rawtime)]
totaltime = sum(timeobj)
formattedtime = ' '.join([''.join(map(lambda x: str(x), i)) for i in list(
zip(processed_rawtime, ['h', 'm', 's'][3-len(processed_rawtime):]))])
if totaltime > currentsong_length:
return ValueError
else:
return (formattedtime, totaltime)
elif rawtime.strip() == '-0':
return ('0', 0)
elif rawtime.isnumeric():
if int(rawtime) > currentsong_length:
return ValueError
processed_rawtime = list(
map(lambda x: int(x), convert(int(rawtime)).split(':')))
formattedtime = ' '.join([''.join(map(lambda x: str(x), i)) for i in list(
zip(processed_rawtime, ['h', 'm', 's'][3-len(processed_rawtime):]))])
# print (None, rawtime)
return (formattedtime, rawtime)
except Exception:
# print (None, None)
return (None, None)
def get_currentsong_length():
global currentsong_length, currentsong_length
if currentsong:
if not currentsong_length and currentsong_length != -1:
currentsong_length = currentsong_length/1000
return currentsong_length
def song_seek(timeval=None, rel_val=None):
global currentsong
if timeval:
try:
vas.vlc_media_player.get_media_player().set_time(int(timeval)*1000)
return True
except Exception:
return None
# raise # TODO - remove all "raise"d exceptions?
elif not rel_val:
SAY(visible=visible, display_message="Error: Can't seek in this audio",
log_message=f'Unsupported codec for seeking audio: {currentsong}', log_priority=2)
return None
def setmastervolume(value=None):
global cached_volume
if comtypes_load_error:
SAY(visible=visible,
log_message="comtypes functionality used even when not available",
display_message="This functionality is unavailable",
log_priority=3)
else:
if not value:
value = cached_volume
if value in range(101):
set_master_volume(value)
else:
SAY(visible=visible, display_message='ERROR: Could not set master volume',
log_message='Could not set master volume', log_priority=2)
def convert(seconds):