├── mpyg321 ├── __init__.py ├── mpyg321.py ├── EventContext.py ├── MPyg321Player.py ├── MpygError.py ├── MPyg123Player.py ├── consts.py └── BasePlayer.py ├── .gitignore ├── setup.py ├── LICENSE ├── .github └── workflows │ └── python-publish.yml ├── examples ├── basic.py ├── events.py └── callbacks.py └── README.md /mpyg321/__init__.py: -------------------------------------------------------------------------------- 1 | name = "mpg321" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | dist 4 | build 5 | mpyg321.egg-info 6 | examples/*.mp3 7 | examples/mpyg321 8 | examples/tests.py 9 | examples/*.txt 10 | venv -------------------------------------------------------------------------------- /mpyg321/mpyg321.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is here for backwards compatibility with previous versions 3 | So that users can use: from mpyg321.mpyg321 import MPyg321Player 4 | Newer version would be: from mpyg321.MPyg321Player import Mpyg321Player 5 | """ 6 | from .MPyg123Player import MPyg123Player 7 | from .MPyg321Player import MPyg321Player 8 | -------------------------------------------------------------------------------- /mpyg321/EventContext.py: -------------------------------------------------------------------------------- 1 | class MPyg321EventContext: 2 | """Base class for all events""" 3 | 4 | def __init__(self, player) -> None: 5 | self.player = player 6 | 7 | 8 | class MPyg321ErrorContext(MPyg321EventContext): 9 | """Context for error events""" 10 | 11 | def __init__(self, player, error_type, error_message) -> None: 12 | super().__init__(player) 13 | self.error_type = error_type 14 | self.error_message = error_message 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="mpyg321", 8 | version="2.2.4", 9 | author="4br3mm0rd", 10 | author_email="4br3mm0rd@gmail.com", 11 | description="mpg321 wrapper for python - command line music player", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/4br3mm0rd/mpyg321", 15 | packages=setuptools.find_packages(), 16 | install_requires=["pexpect"], 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /mpyg321/MPyg321Player.py: -------------------------------------------------------------------------------- 1 | from .BasePlayer import BasePlayer 2 | from .consts import MPyg321Events 3 | 4 | 5 | class MPyg321Player(BasePlayer): 6 | """Player for legacy mpg321""" 7 | 8 | def __init__( 9 | self, player=None, audiodevice=None, performance_mode=True, custom_args="" 10 | ): 11 | self.suitable_versions = ["mpg321"] 12 | self.default_player = "mpg321" 13 | super().__init__(player, audiodevice, performance_mode, custom_args) 14 | 15 | def process_output_ext(self, action): 16 | """ 17 | Processes specific output for mpg321 player 18 | It should contain the code for the mpg_out "end_of_song" 19 | We did not put it because the BaseClass implements a behavior 20 | which works for both versions 21 | """ 22 | pass 23 | 24 | def volume(self, percent): 25 | """Adjust player's volume""" 26 | self.player.sendline("GAIN {}".format(percent)) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | deploy: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package 33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /mpyg321/MpygError.py: -------------------------------------------------------------------------------- 1 | # # # Errors # # # 2 | class MPygError(RuntimeError): 3 | """Base class for any errors encountered by the player during runtime""" 4 | 5 | pass 6 | 7 | 8 | class MPygFileError(MPygError): 9 | """Errors encountered by the player related to files""" 10 | 11 | pass 12 | 13 | 14 | class MPygCommandError(MPygError): 15 | """Errors encountered by the player related to player commands""" 16 | 17 | pass 18 | 19 | 20 | class MPygArgumentError(MPygError): 21 | """Errors encountered by the player related to arguments for commands""" 22 | 23 | pass 24 | 25 | 26 | class MPygEQError(MPygError): 27 | """Errors encountered by the player related to the equalizer""" 28 | 29 | pass 30 | 31 | 32 | class MPygSeekError(MPygError): 33 | """Errors encountered by the player related to the seek""" 34 | 35 | pass 36 | 37 | 38 | class MPygPlayerNotFoundError(MPygError): 39 | """Errors encountered when no suitable player is found""" 40 | 41 | pass 42 | 43 | 44 | class MPygUnknownEventNameError(MPygError): 45 | """Errors encountered when creating an event listener for a non existing event""" 46 | 47 | pass 48 | 49 | 50 | class MPygEventListenerError(MPygError): 51 | """Errors encountered when an event listener throws an exception""" 52 | 53 | pass 54 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | MPyG321 basic example 3 | Playing and pausing some music 4 | You need to add a "sample.mp3" file in the working directory 5 | 6 | In this example, you can replace MPyg321Player by MPyg123Player 7 | according to the player you installed on your machine (mpg321/mpg123) 8 | """ 9 | from mpyg321.MPyg321Player import MPyg321Player 10 | from time import sleep 11 | 12 | 13 | def do_some_play_pause(player): 14 | """Does some play and pause""" 15 | player.play_song("sample.mp3") 16 | sleep(5) 17 | player.pause() 18 | sleep(3) 19 | player.resume() 20 | sleep(5) 21 | player.stop() 22 | 23 | 24 | def do_some_jumps(player): 25 | """Does some jumps""" 26 | player.play_song("sample.mp3") 27 | sleep(3) 28 | print("Jumping to MPEG frame 200...") 29 | player.jump(200) 30 | sleep(3) 31 | print("Jumping to 1 second...") 32 | player.jump("1s") 33 | sleep(3) 34 | print("Jumping forward 20 MPEG frames...") 35 | player.jump("+20") 36 | sleep(3) 37 | print("Jumping back 1 second...") 38 | player.jump("-1s") 39 | sleep(3) 40 | player.stop() 41 | 42 | 43 | def main(): 44 | """Do the magic""" 45 | player = MPyg321Player() 46 | do_some_play_pause(player) 47 | do_some_jumps(player) 48 | player.quit() 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /examples/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | MPyg321 callbacks example 3 | Playing and pausing some music, triggering callbacks 4 | You need to add a "sample.mp3" file in the working directory 5 | 6 | In this example, you can replace MPyg321Player by MPyg123Player 7 | according to the player you installed on your machine (mpg321/mpg123) 8 | """ 9 | 10 | from time import sleep 11 | 12 | from mpyg321.consts import MPyg321Events 13 | from mpyg321.MPyg123Player import MPyg123Player 14 | 15 | player = MPyg123Player() 16 | 17 | 18 | @player.on(MPyg321Events.ANY_STOP) 19 | def on_any_stop(context): 20 | """Callback when the music stops for any reason""" 21 | print("The music has stopped") 22 | print(context) 23 | 24 | 25 | @player.on(MPyg321Events.USER_PAUSE) 26 | def on_user_pause(context): 27 | """Callback when user pauses the music""" 28 | print("The music has paused") 29 | print(context) 30 | 31 | 32 | def on_user_resume(context): 33 | """Callback when user resumes the music""" 34 | print("The music has resumed") 35 | print(context) 36 | 37 | 38 | @player.on(MPyg321Events.USER_STOP) 39 | def on_user_stop(context): 40 | """Callback when user stops music""" 41 | print("The music has stopped (by user)") 42 | print(context) 43 | 44 | 45 | @player.on(MPyg321Events.MUSIC_END) 46 | def on_music_end(context): 47 | """Callback when music ends""" 48 | print("The music has ended") 49 | print(context) 50 | 51 | 52 | def do_some_play_pause(player): 53 | """Does some play and pause""" 54 | player.play_song("sample.mp3") 55 | sleep(5) 56 | player.pause() 57 | player.subscribe_event(MPyg321Events.USER_RESUME, on_user_resume) 58 | sleep(3) 59 | player.resume() 60 | sleep(5) 61 | player.stop() 62 | sleep(2) 63 | player.play() 64 | sleep(20) 65 | player.quit() 66 | 67 | 68 | def main(): 69 | """Do the magic""" 70 | do_some_play_pause(player) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /examples/callbacks.py: -------------------------------------------------------------------------------- 1 | """ 2 | MPyg321 callbacks example 3 | Playing and pausing some music, triggering callbacks 4 | You need to add a "sample.mp3" file in the working directory 5 | 6 | In this example, you can replace MPyg321Player by MPyg123Player 7 | according to the player you installed on your machine (mpg321/mpg123) 8 | """ 9 | from mpyg321.MPyg123Player import MPyg123Player 10 | 11 | from time import sleep 12 | 13 | 14 | class MyPlayer(MPyg123Player): 15 | """We create a class extending the basic player to implement callbacks""" 16 | 17 | def on_any_stop(self): 18 | """Callback when the music stops for any reason""" 19 | print("The music has stopped") 20 | 21 | def on_user_pause(self): 22 | """Callback when user pauses the music""" 23 | print("The music has paused") 24 | 25 | def on_user_resume(self): 26 | """Callback when user resumes the music""" 27 | print("The music has resumed") 28 | 29 | def on_user_stop(self): 30 | """Callback when user stops music""" 31 | print("The music has stopped (by user)") 32 | 33 | def on_music_end(self): 34 | """Callback when music ends""" 35 | print("The music has ended") 36 | 37 | def on_user_mute(self): 38 | """Callback when music is muted""" 39 | print("The music has been muted (continues playing)") 40 | 41 | def on_user_unmute(self): 42 | """Callback when music is unmuted""" 43 | print("Music has been unmuted") 44 | 45 | 46 | def do_some_play_pause(player): 47 | """Does some play and pause""" 48 | player.play_song("sample.mp3") 49 | sleep(5) 50 | player.pause() 51 | sleep(3) 52 | player.resume() 53 | sleep(5) 54 | player.stop() 55 | sleep(2) 56 | player.play() 57 | sleep(2) 58 | player.mute() 59 | sleep(1) 60 | player.unmute() 61 | sleep(20) 62 | player.quit() 63 | 64 | 65 | def main(): 66 | """Do the magic""" 67 | player = MyPlayer(rva_mix=True) 68 | do_some_play_pause(player) 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /mpyg321/MPyg123Player.py: -------------------------------------------------------------------------------- 1 | from .BasePlayer import BasePlayer 2 | from .consts import MPyg321Events, PlayerStatus 3 | from .EventContext import MPyg321EventContext 4 | 5 | 6 | class MPyg123Player(BasePlayer): 7 | """Player for mpg123""" 8 | 9 | def __init__( 10 | self, 11 | player=None, 12 | audiodevice=None, 13 | performance_mode=True, 14 | custom_args="", 15 | rva_mix=False, 16 | ): 17 | self.suitable_versions = ["mpg123"] 18 | self.default_player = "mpg123" 19 | custom_args += " --rva-mix " if rva_mix else "" 20 | super().__init__(player, audiodevice, performance_mode, custom_args) 21 | if performance_mode: 22 | self.silence_mpyg_output() 23 | self._is_muted = False 24 | 25 | def process_output_ext(self, action): 26 | """Processes specific output for mpg123 player""" 27 | if action == "user_mute": 28 | self._is_muted = True 29 | self.on_user_mute() 30 | self._trigger_event(MPyg321Events.USER_MUTE, MPyg321EventContext(self)) 31 | elif action == "user_unmute": 32 | self._is_muted = False 33 | self._trigger_event(MPyg321Events.USER_UNMUTE, MPyg321EventContext(self)) 34 | self.on_user_unmute() 35 | 36 | def load_list(self, entry, filepath): 37 | """Load an entry in a list 38 | Parameters: 39 | entry (int): index of the song in the list - first is 0 40 | filepath: URL/Path to the list 41 | """ 42 | self.player.sendline("LOADLIST {} {}".format(entry, filepath)) 43 | self.status = PlayerStatus.PLAYING 44 | 45 | def silence_mpyg_output(self): 46 | """Improves performance by silencing the mpg123 process frame output""" 47 | self.player.sendline("SILENCE") 48 | 49 | def mute(self): 50 | """Mutes the player""" 51 | self.player.sendline("MUTE") 52 | 53 | def unmute(self): 54 | """Unmutes the player""" 55 | self.player.sendline("UNMUTE") 56 | 57 | def toggle_mute(self): 58 | """Mute or UnMute if playing""" 59 | if self._is_muted: 60 | self.unmute() 61 | else: 62 | self.mute() 63 | 64 | def volume(self, percent): 65 | """Adjust player's volume""" 66 | self.player.sendline("VOLUME {}".format(percent)) 67 | 68 | # # # Public Callbacks # # # 69 | def on_user_mute(self): 70 | """Callback when user mutes player""" 71 | pass 72 | 73 | def on_user_unmute(self): 74 | """Callback when user unmutes player""" 75 | pass 76 | -------------------------------------------------------------------------------- /mpyg321/consts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pexpect import TIMEOUT as pexpectTIMEOUT 4 | 5 | mpg_outs = [ 6 | { 7 | "mpg_code": "@P 0", 8 | "action": "music_stop", 9 | "description": """For mpg123, it corresponds to any stop 10 | For mpg312 it corresponds to user stop only""", 11 | }, 12 | { 13 | "mpg_code": "@P 1", 14 | "action": "user_pause", 15 | "description": "Music has been paused by the user.", 16 | }, 17 | { 18 | "mpg_code": "@P 2", 19 | "action": "user_start_or_resume", 20 | "description": "Music has been started resumed by the user.", 21 | }, 22 | { 23 | "mpg_code": "@E *", 24 | "action": "error", 25 | "description": "Player has encountered an error.", 26 | }, 27 | { 28 | "mpg_code": "@silence", 29 | "action": None, 30 | "description": "Player has been silenced by the user.", 31 | }, 32 | { 33 | "mpg_code": r"@V [0-9\.\s%]*", 34 | "action": None, 35 | "description": "Volume change event.", 36 | }, 37 | { 38 | "mpg_code": r"@S [a-zA-Z0-9\.\s-]*", 39 | "action": None, 40 | "description": "Stereo info event.", 41 | }, 42 | {"mpg_code": "@I *", "action": None, "description": "Information event."}, 43 | {"mpg_code": pexpectTIMEOUT, "action": None, "description": "Timeout event."}, 44 | ] 45 | 46 | mpg_outs_ext = { 47 | "mpg123": [ 48 | { 49 | "mpg_code": "@mute", 50 | "action": "user_mute", 51 | "description": "Player has been muted by the user.", 52 | }, 53 | { 54 | "mpg_code": "@unmute", 55 | "action": "user_unmute", 56 | "description": "Player has been unmuted by the user.", 57 | }, 58 | ], 59 | "mpg321": [ 60 | { 61 | "mpg_code": "@P 3", 62 | "action": "end_of_song", 63 | "description": "Player has reached the end of the song.", 64 | } 65 | ], 66 | } 67 | 68 | mpg_errors = [ 69 | {"message": "empty list name", "action": "generic_error"}, 70 | {"message": "No track loaded!", "action": "generic_error"}, 71 | {"message": "Error opening stream", "action": "file_error"}, 72 | {"message": "failed to parse given eq file:", "action": "file_error"}, 73 | {"message": "Corrupted file:", "action": "file_error"}, 74 | {"message": "Unknown command:", "action": "command_error"}, 75 | {"message": "Unfinished command:", "action": "command_error"}, 76 | {"message": "Unknown command or no arguments:", "action": "argument_error"}, 77 | {"message": "invalid arguments for", "action": "argument_error"}, 78 | {"message": "Missing argument to", "action": "argument_error"}, 79 | {"message": "failed to set eq:", "action": "eq_error"}, 80 | {"message": "Error while seeking", "action": "seek_error"}, 81 | ] 82 | 83 | 84 | class PlayerStatus: 85 | INSTANCIATED = 0 86 | PLAYING = 1 87 | PAUSED = 2 88 | RESUMING = 3 89 | STOPPING = 4 90 | STOPPED = 5 91 | QUITTED = 6 92 | 93 | 94 | class MPyg321Events(Enum): 95 | USER_STOP = "user_stop" 96 | USER_PAUSE = "user_pause" 97 | USER_RESUME = "user_resume" 98 | ANY_STOP = "any_stop" 99 | MUSIC_END = "music_end" 100 | ERROR = "error" 101 | USER_MUTE = "mute" 102 | USER_UNMUTE = "unmute" 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://pepy.tech/badge/mpyg321)](https://pepy.tech/project/mpyg321) 2 | [![Downloads](https://pepy.tech/badge/mpyg321/month)](https://pepy.tech/project/mpyg321) 3 | [![Downloads](https://pepy.tech/badge/mpyg321/week)](https://pepy.tech/project/mpyg321) 4 | 5 | # mpyg321 6 | 7 | mpyg321 is a simple python wrapper for mpg321 and mpg123. It allows you to easily play mp3 sounds in python, do basic operations on the music and implement callbacks for events like the end of a sound. 8 | 9 | # Installation 10 | 11 | mpyg321 requires the installation of mpg123 (or mpg321 depending on your usage) software for reading mp3. This section describes the installation of the library on MacOS, Linux and Windows. 12 | 13 | We recommend using mpg123 since the project is more up to date. However, you can also use this library with mpg321 (using the `MPyg321Player` class) 14 | 15 | ## MacOS 16 | 17 | ``` 18 | $ brew install mpg123 # or mpg321 19 | $ pip3 install mpyg321 20 | ``` 21 | 22 | ## Linux 23 | 24 | ``` 25 | $ sudo apt-get update 26 | $ sudo apt-get install mpg123 # or mpg321 27 | $ pip3 install mpyg321 28 | ``` 29 | 30 | ## Windows 31 | 32 | For windows installation, download mpg123 on the website: [mpg123's website](https://www.mpg123.de/download.shtml), and then run: 33 | 34 | ``` 35 | $ pip install mpyg321 36 | ``` 37 | 38 | # Usage 39 | 40 | Usage is pretty straight forward, and all the functionnalities are easily shown in the examples folder. 41 | 42 | ``` 43 | from mpyg321.MPyg123Player import MPyg123Player # or MPyg321Player if you installed mpg321 44 | player = MPyg123Player() 45 | player.play_song("/path/to/some_mp3.mp3") 46 | ``` 47 | 48 | ## Calbacks and Events 49 | 50 | ### Callbacks 51 | 52 | You can implement callbacks for several events such as: end of song, user paused the music, ... 53 | All the callbacks can be found inside the code of the `BasePlayer` class and the `MPyg123Player` class. 54 | Most of the callbacks are implemented in the `callbacks.py` example file. 55 | 56 | ### Events 57 | 58 | Starting **from version 2.2.0**, you can now subscribe to events using decorators and/or the `subscribe_event` function. 59 | Here is an example usage. You can find more details in the `events.py` example file. 60 | 61 | ``` 62 | player = MPyg123Player() 63 | 64 | @player.on(MPyg321Events.ANY_STOP) 65 | def callback(context): 66 | print("Any stop event occured") 67 | 68 | # or 69 | def my_func(context): 70 | print("Other event subscribed") 71 | 72 | player.subscribe_event(MPyg321Events.ANY_STOP, my_func) 73 | ``` 74 | 75 | Here is an exhaustive list of the available events and their compatibilities with the different players (MPyg123 and MPyg321): 76 | |Event|Description|MPyg123Player|MPyg321Player| 77 | |------|------|:---------:|:-------:| 78 | |USER_STOP|When you stop the music (call `player.stop`)|X|X| 79 | |USER_PAUSE|When you pause the music (call `player.pause`)|X|X| 80 | |USER_RESUME|When you resume the music (call `player.resume`)|X|X| 81 | |ANY_STOP|When any stop occures (pause, stop, end of music)|X|X| 82 | |MUSIC_END|When the music ends|X|X| 83 | |ERROR|When an error occurs (The error info is within the context using the class `MPyg321ErrorContext`)|X|X| 84 | |USER_MUTE|When you mute the player|X|-| 85 | |USER_MUTE|When you unmute the player|X|-| 86 | 87 | ## Loops 88 | 89 | In order to loop (replay the song when it ended), you can either set the loop mode when calling the `play_song` function: 90 | 91 | ``` 92 | player.play_song("/path/to/sample.mp3", loop=True) 93 | ``` 94 | 95 | or programmatically set the loop mode anywhere in the code: 96 | 97 | ``` 98 | player.play_song("/path/to/sample.mp3) 99 | // Do some stuff ... 100 | player.set_loop(True) 101 | ``` 102 | 103 | **Note:** when calling `player.set_loop(True)`, the loop mode will only be taken into account at the end of a song. If nothing is playing, this call will not replay the previous song. In order to replay the previous song, you should call: `player.play()` 104 | -------------------------------------------------------------------------------- /mpyg321/BasePlayer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mpyg BasePlayer base class 3 | This class contains all the functions that are common 4 | both to mpg321 and mpg123. 5 | All the players implement this base class and add their 6 | specific feature. 7 | """ 8 | 9 | import subprocess 10 | from threading import Thread 11 | 12 | import pexpect 13 | 14 | from .consts import * 15 | from .EventContext import * 16 | from .MpygError import * 17 | 18 | 19 | class BasePlayer: 20 | """Base class for players""" 21 | 22 | player = None 23 | status = None 24 | output_processor = None 25 | song_path = "" 26 | loop = False 27 | performance_mode = True 28 | suitable_versions = [] # mpg123 and/or mpg321 - set inside subclass 29 | default_player = None # mpg123 or mpg321 - set inside subclass 30 | player_version = None # defined inside check_player 31 | mpg_outs = [] 32 | _events = {} 33 | 34 | def __init__( 35 | self, player=None, audiodevice=None, performance_mode=True, custom_args="" 36 | ): 37 | """Builds the player and creates the callbacks""" 38 | self._events = {e: [] for e in MPyg321Events} 39 | self.set_player(player, audiodevice, custom_args) 40 | self.output_processor = Thread(target=self.process_output) 41 | self.output_processor.daemon = True 42 | self.performance_mode = performance_mode 43 | self.output_processor.start() 44 | 45 | def check_player(self, player): 46 | """Gets the player""" 47 | try: 48 | cmd = str(player) 49 | output = subprocess.check_output([cmd, "--version"]) 50 | for version in self.suitable_versions: 51 | if version in str(output): 52 | self.player_version = version 53 | if self.player_version is None: 54 | raise MPygPlayerNotFoundError( 55 | """No suitable player found: you might be using the wrong \ 56 | player (Mpyg321Player or Mpyg123Player)""" 57 | ) 58 | except subprocess.SubprocessError: 59 | raise MPygPlayerNotFoundError( 60 | """No suitable player found: you might need to install 61 | mpg123""" 62 | ) 63 | 64 | def set_player(self, player, audiodevice, custom_args): 65 | """Sets the player""" 66 | if player is None: 67 | player = self.default_player 68 | self.check_player(player) 69 | args = " " + custom_args if custom_args != "" else "" 70 | args += " --audiodevice " + audiodevice if audiodevice else "" 71 | args += " -R mpyg" 72 | self.player = pexpect.spawn(str(player) + " " + args) 73 | self.player.delaybeforesend = None 74 | self.status = PlayerStatus.INSTANCIATED 75 | # Setting extended mpg_outs for version specific behaviors 76 | self.mpg_outs = mpg_outs.copy() 77 | self.mpg_outs.extend(mpg_outs_ext[self.player_version]) 78 | 79 | def on(self, event_name): 80 | """Decorator to register event callbacks.""" 81 | 82 | def decorator(func): 83 | if event_name not in self._events: 84 | raise MPygUnknownEventNameError( 85 | f"Subscribed callback to a non existing event {event_name}." 86 | ) 87 | self._events[event_name].append(func) 88 | return func 89 | 90 | return decorator 91 | 92 | def subscribe_event(self, event_name, callback): 93 | if event_name not in self._events: 94 | raise MPygUnknownEventNameError( 95 | f"Subscribed callback to a non existing event {event_name}." 96 | ) 97 | self._events[event_name].append(callback) 98 | 99 | def _trigger_event(self, event_name, context=None): 100 | """Trigger all callbacks associated with an event.""" 101 | if context is None: 102 | context = MPyg321EventContext(self) 103 | if event_name in self._events: 104 | for callback in self._events[event_name]: 105 | try: 106 | callback(context) 107 | except Exception: 108 | raise MPygEventListenerError( 109 | "Error while executiong event callback" 110 | ) 111 | 112 | def process_output(self): 113 | """Parses the output""" 114 | mpg_codes = [v["mpg_code"] for v in self.mpg_outs] 115 | while True: 116 | index = self.player.expect(mpg_codes) 117 | action = self.mpg_outs[index]["action"] 118 | if action == "music_stop": 119 | self.on_music_stop_int() 120 | elif action == "user_pause": 121 | self.on_user_pause_int() 122 | elif action == "user_start_or_resume": 123 | self.on_user_start_or_resume_int() 124 | elif action == "end_of_song": 125 | self.on_end_of_song_int() 126 | elif action == "error": 127 | self.on_error() 128 | else: 129 | self.process_output_ext(action) 130 | 131 | def process_output_ext(self, action): 132 | """Processes the output for version specific behavior""" 133 | pass 134 | 135 | def play_song(self, path, loop=False): 136 | """Plays the song""" 137 | self.loop = loop 138 | self.set_song(path) 139 | self.play() 140 | 141 | def play(self): 142 | """Starts playing the song""" 143 | self.player.sendline("LOAD " + self.song_path) 144 | self.status = PlayerStatus.PLAYING 145 | 146 | def pause(self): 147 | """Pauses the player""" 148 | if self.status == PlayerStatus.PLAYING: 149 | self.player.sendline("PAUSE") 150 | self.status = PlayerStatus.PAUSED 151 | 152 | def toggle_pause(self): 153 | """Pause if playing, else resume if paused""" 154 | if self.status == PlayerStatus.PLAYING: 155 | self.pause() 156 | elif self.status == PlayerStatus.PAUSED: 157 | self.resume() 158 | 159 | def resume(self): 160 | """Resume the player""" 161 | if self.status == PlayerStatus.PAUSED: 162 | self.player.sendline("PAUSE") 163 | self.status = PlayerStatus.PLAYING 164 | self._trigger_event(MPyg321Events.USER_RESUME) 165 | self.on_user_resume() 166 | 167 | def stop(self): 168 | """Stops the player""" 169 | self.player.sendline("STOP") 170 | self.status = PlayerStatus.STOPPING 171 | 172 | def quit(self): 173 | """Quits the player""" 174 | self.player.sendline("QUIT") 175 | self.status = PlayerStatus.QUITTED 176 | 177 | def jump(self, pos): 178 | """Jump to position""" 179 | self.player.sendline("JUMP " + str(pos)) 180 | 181 | def on_error(self): 182 | """Process errors encountered by the player""" 183 | output = self.player.readline().decode("utf-8") 184 | 185 | # Check error in list of errors 186 | for mpg_error in mpg_errors: 187 | if mpg_error["message"] in output: 188 | action = mpg_error["action"] 189 | context = MPyg321ErrorContext(self, action, output) 190 | self._trigger_event(MPyg321Events.ERROR, context) 191 | if action == "generic_error": 192 | raise MPygError(output) 193 | if action == "file_error": 194 | raise MPygFileError(output) 195 | if action == "command_error": 196 | raise MPygCommandError(output) 197 | if action == "argument_error": 198 | raise MPygArgumentError(output) 199 | if action == "eq_error": 200 | raise MPygEQError 201 | if action == "seek_error": 202 | raise MPygSeekError 203 | 204 | # Some other error occurred 205 | context = MPyg321ErrorContext(self, "unknown_error", output) 206 | self._trigger_event(MPyg321Events.ERROR, context) 207 | raise MPygError(output) 208 | 209 | def set_song(self, path): 210 | """song_path setter""" 211 | self.song_path = path 212 | 213 | def set_loop(self, loop): 214 | """loop setter""" 215 | self.loop = loop 216 | 217 | # # # Internal Callbacks # # # 218 | def on_music_stop_int(self): 219 | """Internal callback when the music is stopped""" 220 | if self.status == PlayerStatus.STOPPING: 221 | self.on_user_stop_int() 222 | self.status = PlayerStatus.STOPPED 223 | else: 224 | self.on_end_of_song_int() 225 | 226 | def on_user_stop_int(self): 227 | """Internal callback when the user stops the music.""" 228 | self._trigger_event(MPyg321Events.ANY_STOP) 229 | self.on_any_stop() 230 | self._trigger_event(MPyg321Events.USER_STOP) 231 | self.on_user_stop() 232 | 233 | def on_user_pause_int(self): 234 | """Internal callback when user pauses the music""" 235 | self._trigger_event(MPyg321Events.ANY_STOP) 236 | self.on_any_stop() 237 | self._trigger_event(MPyg321Events.USER_PAUSE) 238 | self.on_user_pause() 239 | 240 | def on_user_start_or_resume_int(self): 241 | """Internal callback when user resumes the music""" 242 | self.status = PlayerStatus.PLAYING 243 | 244 | def on_end_of_song_int(self): 245 | """Internal callback when the song ends""" 246 | if self.loop: 247 | self.play() 248 | else: 249 | # The music doesn't stop if it is looped 250 | self._trigger_event(MPyg321Events.ANY_STOP) 251 | self.status = PlayerStatus.STOPPED 252 | self.on_any_stop() 253 | self._trigger_event(MPyg321Events.MUSIC_END) 254 | self.on_music_end() 255 | 256 | # # # Public Callbacks # # # 257 | def on_any_stop(self): 258 | """Callback when the music stops for any reason""" 259 | pass 260 | 261 | def on_user_pause(self): 262 | """Callback when user pauses the music""" 263 | pass 264 | 265 | def on_user_resume(self): 266 | """Callback when user resumes the music""" 267 | pass 268 | 269 | def on_user_stop(self): 270 | """Callback when user stops music""" 271 | pass 272 | 273 | def on_music_end(self): 274 | """Callback when music ends""" 275 | pass 276 | --------------------------------------------------------------------------------