diff --git a/.gitignore b/.gitignore index 84d186f6..916729dc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ dist/ __pycache__/ venv/ settings.json -test.py \ No newline at end of file +test.py +test.pyw +test.spec \ No newline at end of file diff --git a/README.md b/README.md index eaf0555c..42dc75d1 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,12 @@ TLDR: If you have music all over the place, right click icon in tray and open se This app supports media keys. There might be an issue with skipping though so please let me know! +# Limitations and known issues +- Music control limited to exit if next/previous song is spammed + # Build Instructions 1. Make sure all the required modules are installed 2. Make sure Python scripts folder is on path 3. OPTIONAL: Having Inno Setup installed and `C:\Program Files (x86)\Inno Setup 6\` on path 4. Run build.bat + diff --git a/after_build.py b/after_build.py index 091556a2..a9ed30c1 100644 --- a/after_build.py +++ b/after_build.py @@ -2,7 +2,6 @@ import zipfile default_settings = { - 'version': '1.3.0', # TODO: remove completely 'previous device': None, 'comments': ['Edit only the variables below', 'Restart the program after editing this file!'], 'auto update': True, @@ -20,11 +19,15 @@ with open('dist/settings.json', 'w') as outfile: json.dump(default_settings, outfile, indent=4) -print('Created dist/settings.json!') - with zipfile.ZipFile('dist/Portable.zip', 'w') as zf: zf.write('dist/Music Caster.exe', 'Music Caster.exe') zf.write('dist/Updater.exe', 'Updater.exe') - # zf.write('dist/settings.json', 'settings.json') print('Created dist/Portable.zip') + +with zipfile.ZipFile('dist/Python Files.zip', 'w') as zf: + zf.write('music_caster.py', 'music_caster.pyw') + zf.write('updater.py', 'updater.pyw') + zf.write('Icons/icon.ico', 'icon.ico') + +print('Created dist/Python Files.zip') diff --git a/music_caster.py b/music_caster.py index 34023514..fe7cf693 100644 --- a/music_caster.py +++ b/music_caster.py @@ -16,7 +16,6 @@ from pynput.keyboard import Listener import PySimpleGUIQt as sg # import PySimpleGUIWx as sg -# import PySide2 from random import shuffle import requests from subprocess import Popen @@ -31,7 +30,7 @@ mutex = win32event.CreateMutex(None, False, 'name') last_error = win32api.GetLastError() -if last_error == ERROR_ALREADY_EXISTS: +if last_error == ERROR_ALREADY_EXISTS: # one instance sys.exit() starting_dir = os.path.dirname(os.path.realpath(__file__)) @@ -40,7 +39,7 @@ while True: try: httpd = HTTPServer(('0.0.0.0', PORT), SimpleHTTPRequestHandler) - threading.Thread(target=httpd.serve_forever, daemon=True).start() + threading.Thread(target=httpd.serve_forever, daemon=True).start() # TODO: multiprocess print('Running server') break except OSError: @@ -52,7 +51,6 @@ home_music_dir = str(Path.home()).replace('\\', '/') + '/Music' CURRENT_VERSION = '1.3.0' settings = { # default settings - 'version': CURRENT_VERSION, # TODO: remove completely 'previous device': None, 'comments': ['Edit only the variables below', 'Restart the program after editing this file!'], 'auto update': True, @@ -97,33 +95,61 @@ def save_json(): if (lt_major > major or lt_major == major and lt_minor > minor or lt_major == major and lt_minor == minor and lt_patch > patch): os.chdir(starting_dir) - if settings.get('DEBUG'): + if settings.get('DEBUG') or os.path.exists('updater.py'): Popen('python updater.py') - else: + elif os.path.exists('Updater.exe'): os.startfile('Updater.exe') + elif os.path.exists('updater.pyw'): + Popen('pythonw updater.pyw') sys.exit() USER_NAME = getpass.getuser() shortcut_path = f'C:/Users/{USER_NAME}/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/Music Caster.lnk' shortcut_exists = os.path.exists(shortcut_path) if settings['run on startup'] and not shortcut_exists and not settings.get('DEBUG'): - # C:\Users\maste\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup - target = f'{starting_dir}\\Music Caster.exe' shell = win32com.client.Dispatch('WScript.Shell') shortcut = shell.CreateShortCut(shortcut_path) + if getattr(sys, 'frozen', False): # Running in a bundle + # C:\Users\maste\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup + target = f'{starting_dir}\\Music Caster.exe' + else: # set shortcut to python script; __file__ + bat_file = f'{starting_dir}\\music_caster.bat' + if os.path.exists(bat_file): + with open('music_caster.bat', 'w') as f: + f.write(f'pythonw {os.path.basename(__file__)}') + target = bat_file + shortcut.IconLocation = f'{starting_dir}\\icon.ico' shortcut.Targetpath = target shortcut.WorkingDirectory = starting_dir shortcut.WindowStyle = 1 # 7 - Minimized, 3 - Maximized, 1 - Normal shortcut.save() + elif not settings['run on startup'] and shortcut_exists: os.remove(shortcut_path) +# device_names = ['1. Local Device'] +# chromecasts = [] +# +# +# def chrome_cast_callback(chromecast): +# chromecasts.append(chromecast) +# devices = len(device_names) +# device_names.append(f'{devices + 2}. {chromecast.device.friendly_name}') +# +# if playing_status == 'PLAYING': tray.Update(menu=menu_def_2) +# elif playing_status == 'PAUSED': tray.Update(menu=menu_def_3) +# else: tray.Update(menu=menu_def_1) + + previous_device = settings['previous device'] local_music_player.init() print('Retrieving chromecasts...') chromecasts = pychromecast.get_chromecasts() print('Retrieved chromecasts') - +try: + cast = next(cc for cc in chromecasts if str(cc.device.uuid) == previous_device) + cast.wait() +except StopIteration: cast = None # device_names = [f'{i + 1}. {cc.device.friendly_name}' for i, cc in enumerate(chromecasts)] device_names = ['1. Local Device'] + [f'{i + 2}. {cc.device.friendly_name}' for i, cc in enumerate(chromecasts)] menu_def_1 = ['File', ['Select &Device', device_names, 'Settings', 'Play &File', 'Play All', 'E&xit']] @@ -134,24 +160,10 @@ def save_json(): menu_def_3 = ['File', ['Select &Device', device_names, 'Settings', 'Play &File', 'Play All', 'Next Song', 'Previous Song', 'Resume', 'E&xit']] - -unfilled_logo_path = f'{starting_dir}/Icons/White Cast Icon.png' -filled_logo_path = f'{starting_dir}/Icons/White Cast Icon Filled.png' - -try: cast = next(cc for cc in chromecasts if str(cc.device.uuid) == previous_device) -except StopIteration: cast = None - unfilled_logo_data = b'iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAAAXNSR0IArs4c6QAABMpJREFUeAHt\nWztrFFEY3ZXEKoooBvGBiC98IDZaCGqKINiInTZWESwstfMHKCKkUCEQbbRRRFBLkWgwWKmFKL4g\nIIqKoIIKvrOeIzu7d8bvzJ01O/u8Hxzmzve49/tOZu69M5spFIIEBgIDgYHAQGAgMBAYCAwEBgwG\nioauUCqVqB8qYz2OfZZfB+g+o4ZHwFmiWCyWkjX9QxDIWQCn88Bg0rnDz2+gvn0g6a1bZ4yg8pVz\nHQ7dRk7ECUna4V5JMyJL+cjbqlvJIQWsnRxUJEnQ/oqlexsxgpK3GCetTp2Qs/7Jv+AWmxU5Jwn6\nZxanIwJiflFwux8x53rrTd5i7V5z3fMPBHkoDQQFgjwMeMzhCgoEeRjwmMMVFAjyMOAx97j2Tt0Q\nujXW2g63mIexQFAgyMOAxxyuoECQhwGPObaK4en/O/z5Toj4ADwHngKPgQmscq9w7CqJvedR70cc\nRkjYGHAJuAnCphxb2zVVvXK7w4Aa5CV8jwJL2o6ZcsKqVlmPCvDov8M+CiyXHbeoQdUl01UBGfU/\n4XcCaJt32qquvAiKxnuFxm45SAsZooSTRzfF5CQ9E0a+0Z8NLAVWA+uAAYA/Qcf8cZ4mp2E8hAmP\nK2NLComxEpOTtOUc6dDXfGAIuAVMAVnkHpxadhJXBUQ1//cRHa8ELgJZiOJqt/a/B8sxEHmZUrch\n0fsmYMwcJa58j9PNdRu4Th3FU6ye1an7ajfoeicwWR3CbJGklrqSzCyhrFZWxxb6nQdwfkoT3m4t\nMyepRF1aYqsSAvgM9toBn8GuYVZ/4AapNuJ7YTsJHFA+0N8HtrTC6kaCrDzlKsYAIbx9hoENVodJ\nHfwOAr8AJaeSMc04V8nJXFSAo/+N9jmAe6RUgQ9JSpOmbyZVcrIwFWDov0F3HODGUgrsI0ZspOKO\nu6mPJVEiyWNaQUlf3/kEHPpVh7D1AmkT9wkV2wi9Kk6OrQI8+hewb1SdwsbVTW0B+IC7QsXmrcfY\npshx4d0HrAIGgMPAbYDzjk8+wyGNJO6TlJyRCeVsUAnVNCw66Qc433xVHZb1vJLSbje14/6BuKbs\njVQ9NREUObMI4LLqtKznnGRO3NDzsWSq7Jc8HIvGaeQxmUR0Pq0c0MkRQBXKMY6rAWC7QAdDuMNu\n+E9QRh5/VSr/zHr0shdQJHELYO6ToOdbABU3mDmBOjkiF1Pq0j165pWk5JwaBAFq2R9RMXnpVfJy\nPARwNXoCjALevyh81JzElc98LIF+CLDkmUwsJ4OVBHVyOCNgHLpVKgA2TtxqdRu24uDPN5PqNlts\nxeSlQx6myPFM71LpI/TbVRBs3AJYMpkS88AKgG6vislDL3KIXUFZVo45SO4KOlNXEh8XrF9YlyHG\nvM3gPy4KXiP0BVXMdPRiLL7yqUgWguhMkkYrUU4D707e4fSOo3Kbu9wTp/3QabtN/orSbOEHdhXJ\nShADtuGvpSbuq5Ue4w11RfAfIixZaSkbrDvrjlcLQYzb4wY77btO220udE+c9gun7TbnuidNaPOD\numkRtFUk/VroFUGfhH/lMyRhz1NNcvhJZmyS7qlxxEXC/43QK4JiE6ET22iCvmBszoe8asyPep3c\nQjMwEBgIDAQGAgOBgcBAYCAwUGXgD8G4G3+pxN+sAAAAAElFTkSuQmCC\n' filled_logo_data = b'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\n' -tray_1 = sg.SystemTray(menu=menu_def_1, data_base64=unfilled_logo_data, tooltip='Music Caster') -tray_2 = sg.SystemTray(menu=menu_def_2, data_base64=filled_logo_data, tooltip='Music Caster') -tray_3 = sg.SystemTray(menu=menu_def_3, data_base64=filled_logo_data, tooltip='Music Caster') -# TODO: tray.Update() -tray_2.Hide() -tray_3.Hide() -tray = tray_1 - +tray = sg.SystemTray(menu=menu_def_1, data_base64=unfilled_logo_data, tooltip='Music Caster') music_directories = settings['music directories'] if not music_directories: music_directories = settings['music directories'] = [home_music_dir] @@ -165,7 +177,7 @@ def save_json(): song_end = song_length = song_position = song_start = 0 playing_status = 'NOT PLAYING' -# select_file_layout = [[sg.InputText(default_text='Audio File', disabled=True), +# sample_layout = [[sg.InputText(default_text='Audio File', disabled=True), # sg.FileBrowse(button_text='Select File', initial_folder=MUSIC_DIR, size=(10, 1), # file_types=(('Audio', '*mp3'),), button_color=('black', 'cyan'))], # [sg.Button('Play!', button_color=('black', 'cyan'), size=(10, 1)), @@ -198,7 +210,9 @@ def play_file(filename, position=0): url = f'http://192.168.2.17:{PORT}/{uri_safe}' cast.wait() mc = cast.media_controller - if mc.is_playing or mc.is_paused: mc.stop() + if mc.is_playing or mc.is_paused: + mc.stop() + mc.block_until_active(5) mc.play_media(url, 'audio/mp3', title=f'{artist} - {title}', current_time=position) # NOTE: tested on Google Home Mini. # TODO: test on chromecast mc.block_until_active() @@ -209,13 +223,11 @@ def play_file(filename, position=0): playing_status = 'PLAYING' -# NOTE: there might be a bug if the media has been paused for a while def pause(): global tray, playing_status, song_position if playing_status == 'PLAYING': - tray.Hide() - tray = tray_3 - tray.UnHide() + tray.Update(menu=menu_def_3, data_base64=unfilled_logo_data) + # tray: sg.SystemTray try: if mc is not None: mc.update_status() @@ -233,9 +245,7 @@ def pause(): def resume(): global tray, playing_status, song_end, song_position if playing_status == 'PAUSED': - tray.Hide() - tray = tray_2 - tray.UnHide() + tray.Update(menu=menu_def_2, data_base64=filled_logo_data) try: if mc is not None: mc.update_status() @@ -249,12 +259,6 @@ def resume(): play_file(music_queue[0], position=song_position) -def play_pause_media_key(): - global playing_status - if playing_status == 'PLAYING': pause() - elif playing_status == 'PAUSED': resume() - - def next_song(): global playing_status if music_queue: @@ -275,52 +279,62 @@ def previous(): play_file(song) -def switch_tray(hide_tray, show_tray): - if hide_tray != show_tray: - hide_tray.Hide() - show_tray.UnHide() - return show_tray - - def on_press(key): + global keyboard_command if str(key) == '<179>': - play_pause_media_key() - if str(key) == '<176>': - next_song() - if str(key) == '<177>': - previous() - - + if playing_status == 'PLAYING': + keyboard_command = 'Pause' + # pause() + elif playing_status == 'PAUSED': + keyboard_command = 'Resume' + # resume() + elif str(key) == '<176>': + keyboard_command = 'Next Song' + # next_song() + elif str(key) == '<177>': + keyboard_command = 'Previous Song' + # previous() + + +keyboard_command = None listener_thread = Listener(on_press=on_press) listener_thread.start() - while True: menu_item = tray.Read(timeout=100) # if menu_item != '__TIMEOUT__': # print(menu_item) if menu_item == 'Exit': + tray.Hide() if cast is not None and cast.app_id == 'CC1AD845': - cast.media_controller.stop() + cast.quit_app() # TODO: implement fadeout? elif local_music_player.music.get_busy(): # local_music_player.music.fadeout(3) # needs to be threaded local_music_player.music.stop() break elif menu_item == 'Play File': - tray.Hide() - tray = tray_2 - tray_2.UnHide() # maybe add *flac compatibility https://mutagen.readthedocs.io/en/latest/api/flac.html - path_to_file = sg.PopupGetFile('Select Music file', file_types=(('Audio', '*mp3'),), initial_folder=DEFAULT_DIR, - no_window=True) + path_to_file = sg.PopupGetFile('Select Music file', file_types=(('Audio', '*mp3'),), + initial_folder=DEFAULT_DIR, no_window=True) if os.path.exists(path_to_file): play_file(path_to_file) + music_queue.clear() + done_queue.clear() for directory in music_directories: - music_queue.extend([file for file in glob(f'{directory}/*.mp3')]) - if path_to_file in music_queue: music_queue.remove(path_to_file) + music_queue.extend([file for file in glob(f'{directory}/*.mp3') if file != path_to_file]) shuffle(music_queue) music_queue.insert(0, path_to_file) + tray.Update(menu=menu_def_2, data_base64=filled_logo_data) + elif menu_item == 'Play All': + music_queue.clear() + for directory in music_directories: + music_queue.extend(file for file in glob(f'{directory}/*.mp3')) + if music_queue: + shuffle(music_queue) + done_queue.clear() + play_file(music_queue[0]) + tray.Update(menu=menu_def_2, data_base64=filled_logo_data) elif menu_item.split('.')[0].isdigit(): # if user selected a device device = ' '.join(menu_item.split('.')[1:])[1:] try: @@ -347,26 +361,14 @@ def on_press(key): play_file(music_queue[0], position=current_pos) elif menu_item == 'Settings': os.startfile(settings_file) - elif menu_item == 'Next Song': + elif 'Next Song' in (menu_item, keyboard_command): next_song() - elif menu_item == 'Previous Song': + elif 'Previous Song' in (menu_item, keyboard_command): previous() - elif menu_item == 'Resume': + elif 'Resume' in (menu_item, keyboard_command): resume() - elif menu_item == 'Pause': + elif 'Pause' in (menu_item, keyboard_command): pause() - elif menu_item == 'Play All': - music_queue.clear() - for directory in music_directories: - music_queue.extend(file for file in glob(f'{directory}/*.mp3')) - if music_queue: - shuffle(music_queue) - done_queue.clear() - play_file(music_queue[0]) - if tray != tray_2: - tray.Hide() - tray = tray_2 - tray.UnHide() elif playing_status == 'PLAYING' and time() > song_end: next_song() elif menu_item == 'Stop': @@ -376,3 +378,4 @@ def on_press(key): elif local_music_player.music.get_busy(): local_music_player.music.stop() playing_status = 'STOPPED' + if keyboard_command is not None: keyboard_command = None diff --git a/requirements.txt b/requirements.txt index 8bef0802..d06d6d4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ PyInstaller pynput PySide2 PySimpleGUIQt +PySimpleGUIWx requests pypiwin32 \ No newline at end of file diff --git a/updater.py b/updater.py index cd749b2b..ed17e677 100644 --- a/updater.py +++ b/updater.py @@ -4,8 +4,10 @@ import zipfile import io import os +import sys from time import sleep from contextlib import suppress +from subprocess import Popen with suppress(FileNotFoundError): @@ -20,20 +22,32 @@ release_entry = soup.find('div', class_='release-entry') latest_version = release_entry.find('a', class_='muted-link css-truncate')['title'][1:] details = release_entry.find('details', class_='details-reset Details-element border-top pt-3 mt-4 mb-2 mb-md-4') - download_link = [link['href'] for link in details.find_all('a') if link.get('href')][1] - download_link = f'https://github.com{download_link}' + download_links = [link['href'] for link in details.find_all('a') if link.get('href')] + bundle_download_link = f'https://github.com{download_links[1]}' + source_download_link = f'https://github.com{download_links[-2]}' with open('settings.json', 'w') as outfile: # TODO: remove completely settings['version'] = latest_version json.dump(settings, outfile, indent=4) if settings.get('DEBUG'): - print(download_link) - from subprocess import Popen + print(bundle_download_link) + print(source_download_link) Popen('python music_caster.py') - else: - r = requests.get(download_link, stream=True) + elif os.path.exists('music_caster.pyw'): # Update python file + r = requests.get(source_download_link, stream=True) + z = zipfile.ZipFile(io.BytesIO(r.content)) + z.extract(f'music-caster-{latest_version}/music_caster.py') + z.close() + if os.path.exists('music_caster.pyw'): + os.remove('music_caster.pyw') + os.rename(f'music-caster-{latest_version}/music_caster.py', 'music_caster.pyw') + os.rmdir(f'music-caster-{latest_version}') + Popen('pythonw music_caster.pyw') + elif os.path.exists('Music Caster.exe'): # Update the bundle + r = requests.get(bundle_download_link, stream=True) z = zipfile.ZipFile(io.BytesIO(r.content)) z.extract('Music Caster.exe') z.close() os.startfile('Music Caster.exe') + # music_caster