diff --git a/src/platformdirs/__init__.py b/src/platformdirs/__init__.py index 2b61b5d2..8eb7273f 100644 --- a/src/platformdirs/__init__.py +++ b/src/platformdirs/__init__.py @@ -257,6 +257,13 @@ def user_videos_dir() -> str: return PlatformDirs().user_videos_dir +def user_music_dir() -> str: + """ + :returns: music directory tied to the user + """ + return PlatformDirs().user_music_dir + + def user_runtime_dir( appname: str | None = None, appauthor: str | None | Literal[False] = None, @@ -494,6 +501,13 @@ def user_videos_path() -> Path: return PlatformDirs().user_videos_path +def user_music_path() -> Path: + """ + :returns: music path tied to the user + """ + return PlatformDirs().user_music_path + + def user_runtime_path( appname: str | None = None, appauthor: str | None | Literal[False] = None, @@ -532,6 +546,7 @@ def user_runtime_path( "user_documents_dir", "user_pictures_dir", "user_videos_dir", + "user_music_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", @@ -544,6 +559,7 @@ def user_runtime_path( "user_documents_path", "user_pictures_path", "user_videos_path", + "user_music_path", "user_runtime_path", "site_data_path", "site_config_path", diff --git a/src/platformdirs/__main__.py b/src/platformdirs/__main__.py index d19fee2d..77543429 100644 --- a/src/platformdirs/__main__.py +++ b/src/platformdirs/__main__.py @@ -11,6 +11,7 @@ "user_documents_dir", "user_pictures_dir", "user_videos_dir", + "user_music_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index 721a8165..eda0de73 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -86,6 +86,13 @@ def user_videos_dir(self) -> str: """ return _android_videos_folder() + @property + def user_music_dir(self) -> str: + """ + :return: music directory tied to the user e.g. ``/storage/emulated/0/Music`` + """ + return _android_music_folder() + @property def user_runtime_dir(self) -> str: """ @@ -167,6 +174,22 @@ def _android_videos_folder() -> str: return videos_dir +@lru_cache(maxsize=1) +def _android_music_folder() -> str: + """:return: music folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + Context = autoclass("android.content.Context") # noqa: N806 + Environment = autoclass("android.os.Environment") # noqa: N806 + music_dir: str = Context.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + except Exception: + music_dir = "/storage/emulated/0/Music" + + return music_dir + + __all__ = [ "Android", ] diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index cf1d4e7f..4a39085d 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -133,6 +133,11 @@ def user_pictures_dir(self) -> str: def user_videos_dir(self) -> str: """:return: videos directory tied to the user""" + @property + @abstractmethod + def user_music_dir(self) -> str: + """:return: music directory tied to the user""" + @property @abstractmethod def user_runtime_dir(self) -> str: @@ -193,6 +198,11 @@ def user_videos_path(self) -> Path: """:return: videos path tied to the user""" return Path(self.user_videos_dir) + @property + def user_music_path(self) -> Path: + """:return: music path tied to the user""" + return Path(self.user_music_dir) + @property def user_runtime_path(self) -> Path: """:return: runtime path tied to the user""" diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py index 12cd9e63..5e66384b 100644 --- a/src/platformdirs/macos.py +++ b/src/platformdirs/macos.py @@ -69,6 +69,11 @@ def user_videos_dir(self) -> str: """:return: videos directory tied to the user, e.g. ``~/Movies``""" return os.path.expanduser("~/Movies") + @property + def user_music_dir(self) -> str: + """:return: music directory tied to the user, e.g. ``~/Music``""" + return os.path.expanduser("~/Music") + @property def user_runtime_dir(self) -> str: """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``""" diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 7358b84f..3813590d 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -143,6 +143,13 @@ def user_videos_dir(self) -> str: """ return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos") + @property + def user_music_dir(self) -> str: + """ + :return: music directory tied to the user, e.g. ``~/Music`` + """ + return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music") + @property def user_runtime_dir(self) -> str: """ diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index 835b336c..e3d2b4f3 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -115,6 +115,13 @@ def user_videos_dir(self) -> str: """ return os.path.normpath(get_win_folder("CSIDL_MYVIDEO")) + @property + def user_music_dir(self) -> str: + """ + :return: music directory tied to the user e.g. ``%USERPROFILE%\\Music`` + """ + return os.path.normpath(get_win_folder("CSIDL_MYMUSIC")) + @property def user_runtime_dir(self) -> str: """ @@ -127,14 +134,9 @@ def user_runtime_dir(self) -> str: def get_win_folder_from_env_vars(csidl_name: str) -> str: """Get folder from environment variables.""" - if csidl_name == "CSIDL_PERSONAL": # does not have an environment name - return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") - - if csidl_name == "CSIDL_MYPICTURES": # does not have an environment name - return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") - - if csidl_name == "CSIDL_MYVIDEO": # does not have an environment name - return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") + result = get_win_folder_if_csidl_name_not_env_var(csidl_name) + if result is not None: + return result env_var_name = { "CSIDL_APPDATA": "APPDATA", @@ -149,6 +151,22 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str: return result +def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: + """Get folder for a CSIDL name that does not exist as an environment variable.""" + if csidl_name == "CSIDL_PERSONAL": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") + + if csidl_name == "CSIDL_MYPICTURES": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") + + if csidl_name == "CSIDL_MYVIDEO": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") + + if csidl_name == "CSIDL_MYMUSIC": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") + return None + + def get_win_folder_from_registry(csidl_name: str) -> str: """Get folder from the registry. @@ -163,6 +181,7 @@ def get_win_folder_from_registry(csidl_name: str) -> str: "CSIDL_PERSONAL": "Personal", "CSIDL_MYPICTURES": "My Pictures", "CSIDL_MYVIDEO": "My Video", + "CSIDL_MYMUSIC": "My Music", }.get(csidl_name) if shell_folder_name is None: raise ValueError(f"Unknown CSIDL name: {csidl_name}") @@ -184,6 +203,7 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: "CSIDL_PERSONAL": 5, "CSIDL_MYPICTURES": 39, "CSIDL_MYVIDEO": 14, + "CSIDL_MYMUSIC": 13, }.get(csidl_name) if csidl_const is None: raise ValueError(f"Unknown CSIDL name: {csidl_name}") diff --git a/tests/conftest.py b/tests/conftest.py index 8ec9c584..752c6201 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ "user_documents_dir", "user_pictures_dir", "user_videos_dir", + "user_music_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", diff --git a/tests/test_android.py b/tests/test_android.py index 60a52265..7d4b2050 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -54,6 +54,7 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No "user_documents_dir": "/storage/emulated/0/Documents", "user_pictures_dir": "/storage/emulated/0/Pictures", "user_videos_dir": "/storage/emulated/0/DCIM/Camera", + "user_music_dir": "/storage/emulated/0/Music", "user_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else '/tmp'}", } expected = expected_map[func] diff --git a/tests/test_macos.py b/tests/test_macos.py index 0d9a3124..f965340a 100644 --- a/tests/test_macos.py +++ b/tests/test_macos.py @@ -35,6 +35,7 @@ def test_macos(params: dict[str, Any], func: str) -> None: "user_documents_dir": f"{home}/Documents", "user_pictures_dir": f"{home}/Pictures", "user_videos_dir": f"{home}/Movies", + "user_music_dir": f"{home}/Music", "user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", } expected = expected_map[func] diff --git a/tests/test_unix.py b/tests/test_unix.py index 111ddf08..6d552642 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -13,7 +13,7 @@ from platformdirs.unix import Unix -@pytest.mark.parametrize("prop", ["user_documents_dir", "user_pictures_dir", "user_videos_dir"]) +@pytest.mark.parametrize("prop", ["user_documents_dir", "user_pictures_dir", "user_videos_dir", "user_music_dir"]) def test_user_media_dir(mocker: MockerFixture, prop: str) -> None: example_path = "/home/example/ExampleMediaFolder" mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") @@ -27,6 +27,7 @@ def test_user_media_dir(mocker: MockerFixture, prop: str) -> None: pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", id="user_documents_dir"), pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", id="user_pictures_dir"), pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", id="user_videos_dir"), + pytest.param("XDG_MUSIC_DIR", "user_music_dir", id="user_music_dir"), ], ) def test_user_media_dir_env_var(mocker: MockerFixture, env_var: str, prop: str) -> None: @@ -46,6 +47,7 @@ def test_user_media_dir_env_var(mocker: MockerFixture, env_var: str, prop: str) pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", "/home/example/Documents", id="user_documents_dir"), pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", "/home/example/Pictures", id="user_pictures_dir"), pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", "/home/example/Videos", id="user_videos_dir"), + pytest.param("XDG_MUSIC_DIR", "user_music_dir", "/home/example/Music", id="user_music_dir"), ], ) def test_user_media_dir_default(mocker: MockerFixture, env_var: str, prop: str, default_abs_path: str) -> None: