Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,9 @@ add option `--web-use-output-dir`, which will make output directory follow `--ou

### Config file location

The config file is located at `C:\Users\user\.spotdl\config.json`
or `~/.config/spotdl/config.json` under Linux
The config file is located at `%USERPROFILE%\AppData\Local\spotdl\config.json`
or `$XDG_CONFIG_HOME/spotdl/config.json` under Linux (usually
`XDG_CONFIG_HOME=~/.config`)

> Note: Prior to v4.4.3 the default Linux location was `~/.spotdl/config.json` which will be used if the new directory doesn't exist.

Expand Down Expand Up @@ -273,7 +274,7 @@ If you don't want the config to load automatically, change the `load_config` opt
"auth_token": null,
"user_auth": false,
"headless": false,
"cache_path": "/Users/username/.spotdl/.spotipy",
"cache_path": "/home/username/.cache/spotdl/spotipy",
"no_cache": false,
"max_retries": 3,
"use_cache_file": false,
Expand Down Expand Up @@ -407,7 +408,7 @@ Main options:
to use `--generate-lrc` option.
--genius-access-token GENIUS_TOKEN
Lets you choose your own Genius access token.
--config Use the config file to download songs. It's located under C:\Users\user\.spotdl\config.json or ~/.spotdl/config.json under linux
--config Use the config file to download songs. It's located under %USERPROFILE%\AppData\Local\spotdl\config.json or $XDG_CONFIG_HOME/spotdl/config.json under linux
--search-query SEARCH_QUERY
The search query to use, available variables: {title}, {artists}, {artist}, {album}, {album-artist}, {genre}, {disc-number}, {disc-count}, {duration}, {year},
{original-date}, {track-number}, {tracks-count}, {isrc}, {track-id}, {publisher}, {list-length}, {list-position}, {list-name}, {output-ext}
Expand All @@ -432,7 +433,7 @@ Spotify options:
--max-retries MAX_RETRIES
The maximum number of retries to perform when getting metadata.
--headless Run in headless mode.
--use-cache-file Use the cache file to get metadata. It's located under C:\Users\<user>\.spotdl\.spotify_cache or ~/.spotdl/.spotify_cache under linux. It only caches tracks and
--use-cache-file Use the cache file to get metadata. It's located under %USERPROFILE%\AppData\Local\spotdl\Cache\spotify_cache or $XDG_CACHE_HOME/spotdl/spotify_cache under linux. It only caches tracks and
gets updated whenever spotDL gets metadata from Spotify. (It may provide outdated metadata use with caution)

FFmpeg options:
Expand Down
4 changes: 2 additions & 2 deletions spotdl/console/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from spotdl._version import __version__
from spotdl.types.options import DownloaderOptions, WebOptions
from spotdl.utils.config import get_spotdl_path, get_web_ui_path
from spotdl.utils.config import get_state_path, get_web_ui_path
from spotdl.utils.logging import NAME_TO_LEVEL
from spotdl.utils.web import (
ALLOWED_ORIGINS,
Expand Down Expand Up @@ -154,7 +154,7 @@ def handle_shutdown(signum, frame): # pylint: disable=unused-argument
not app_state.web_settings["keep_sessions"]
and not app_state.web_settings["web_use_output_dir"]
):
sessions_dir = Path(get_spotdl_path() / "web/sessions")
sessions_dir = Path(get_state_path() / "web" / "sessions")
logger.info("Removing sessions directories")
if sessions_dir.exists():
shutil.rmtree(sessions_dir)
8 changes: 4 additions & 4 deletions spotdl/utils/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ def parse_main_options(parser: _ArgumentGroup):
action="store_true",
help=(
"Use the config file to download songs. "
"It's located under C:\\Users\\user\\.spotdl\\config.json "
"or ~/.spotdl/config.json under linux"
"It's located under %USERPROFILE%\\AppData\\Local\\spotdl\\config.json "
"or $XDG_CONFIG_HOME/spotdl/config.json under linux"
),
)

Expand Down Expand Up @@ -252,8 +252,8 @@ def parse_spotify_options(parser: _ArgumentGroup):
const=True,
help=(
"Use the cache file to get metadata. "
"It's located under C:\\Users\\user\\.spotdl\\.spotify_cache "
"or ~/.spotdl/.spotify_cache under linux. "
"It's located under %USERPROFILE%\\AppData\\Local\\spotdl\\Cache\\spotify_cache "
"or $XDG_CACHE_HOME/spotdl/spotify_cache under linux. "
"It only caches tracks and "
"gets updated whenever spotDL gets metadata from Spotify. "
"(It may provide outdated metadata use with caution)"
Expand Down
214 changes: 151 additions & 63 deletions spotdl/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

import json
import logging
import os
import platform
import platformdirs as pd
from argparse import Namespace
from pathlib import Path
from typing import Any, Dict, Tuple, Union
from typing import Any, Dict, Tuple, Union, TypeVar

from spotdl.types.options import (
DownloaderOptions,
Expand All @@ -19,12 +19,15 @@
WebOptions,
)

T = TypeVar("T")

__all__ = [
"ConfigError",
"get_spotdl_path",
"get_config_file",
"get_cache_path",
"get_state_path",
"get_temp_path",
"get_utils_path",
"get_spotipy_client_cache_path",
"get_errors_path",
"get_web_ui_path",
"get_config",
Expand All @@ -45,91 +48,175 @@ class ConfigError(Exception):
"""


def get_spotdl_path() -> Path:
def get_path_fallback(default: Path, *fallbacks: Path) -> Path:
"""
Get the path to the spotdl folder, following XDG standards on Linux.
~/.config/spotdl/ is used if it exists, else ~/.spotdl if it exists.
If the spotdl directory does not exist, it will be created
Get the first path among current and fallbacks that exists.
If none exist, create current and return it.
All the paths passed must refer to directories.

### Returns
- The path to the spotdl folder.
- A path, guaranteed to exist, the first such among default and fallbacks.
### Note
- This has the implication that for fallbacks for files, we pick behaviours according to whether
their _parent directory_ exists. For our current purposes, this is sufficient, since we need
to be able to raise ConfigError if the config file doesn't exist, and are so far not moving
files into subdirectories of existing fallback paths.
(To clarify -- the identified failure mode is moving ~/.spotdlrc to ~/.spotdl/spotdlrc)
"""

### Notes
for path in default, *fallbacks:
if path.exists():
return path
default.mkdir(parents=True, exist_ok=True)
return default


def elem_if(x: T, p: bool) -> Iterable[T]:
"""
Yields x if p is true.
Useful for inserting x into a list only when p is true.
"""
if p:
yield x


# Backwards-compatibility for paths under the old behaviour where spotdl would put everything under
# a "spotdl folder" and all paths would be relative to these root directories.
#
# In order for this list to be affected by the monkeypatching to `os.environ` in tests, we need to
# wrap it in a function so it gets reevaluated every time.
# This way, it picks up the changes to the environment.
def old_spotdl_dirs():
return [
# An initial attempt at XDG Base Directory support hardcoded the path as ~/.config/spotdl on
# linux
*(elem_if(Path.home() / ".config" / "spotdl", platform.system() == "Linux")),
# The previous behaviour, which unconditionally used the unixy ~/.spotdl
Path.home() / ".spotdl",
]

# For Linux systems, we follow the XDG Base Directory Specification
if platform.system() == "Linux":
# Define the new, correct XDG config path (~/.config/spotdl)
xdg_config_path = Path.home() / ".config" / "spotdl"

# Define the old path (~/.spotdl) for backward compatibility
old_spotdl_path = Path.home() / ".spotdl"
def get_config_file() -> Path:
"""
Get config file path

# Scenario 1: The user already has the new XDG config folder. Use it.
if xdg_config_path.exists():
return xdg_config_path
### Returns
- The path to the config file.
"""

# Scenario 2: The user is an existing user with only the old folder. Use the old one.
if old_spotdl_path.exists():
return old_spotdl_path
return get_path_fallback(
*(
root / file
for root in [pd.user_config_path(appname="spotdl")] + old_spotdl_dirs()
for file in ["config.json"]
)
)

# Scenario 3: The user is brand new. Create and use the new XDG path.
os.makedirs(xdg_config_path, exist_ok=True)
return xdg_config_path

# For non-Linux systems (like Windows), use the default ~/.spotdl path
spotdl_path = Path.home() / ".spotdl"
os.makedirs(spotdl_path, exist_ok=True)
def get_state_path() -> Path:
"""
Get the path to the state folder.

return spotdl_path
### Returns
- The path to the directory containing working state.
"""

return get_path_fallback(
*(
root / dir
for root in [pd.user_state_path(appname="spotdl")] + old_spotdl_dirs()
for dir in [""]
)
)

def get_config_file() -> Path:

def get_temp_path() -> Path:
"""
Get config file path
Get the path to the temp folder.

### Returns
- The path to the config file.
- The path to the temp folder.
"""

return get_spotdl_path() / "config.json"
return get_path_fallback(
*(
root / dir
for root in [pd.user_cache_path(appname="spotdl")] + old_spotdl_dirs()
for dir in ["temp"]
)
)


def get_cache_path() -> Path:
def get_utils_path() -> Path:
"""
Get the path to the cache folder.
Get the path to the utilities folder.

### Returns
- The path to the spotipy cache file.
- The path to the directory containing support programs.
"""

return get_spotdl_path() / ".spotipy"
return get_path_fallback(
*(
[
root / dir
for root in [pd.user_data_path(appname="spotdl")]
for dir in ["utils"]
]
+ [
root / dir
for root in old_spotdl_dirs()
for dir in [""]
]
)
)


def get_spotify_cache_path() -> Path:
def get_spotipy_client_cache_path() -> Path:
"""
Get the path to the spotify cache folder.
Get the path to the cache folder.

### Returns
- The path to the spotipy cache file.
"""

return get_spotdl_path() / ".spotify_cache"
return get_path_fallback(
*(
[
root / dir
for root in [pd.user_cache_path(appname="spotdl")]
for dir in ["spotipy", ".spotipy"]
]
+ [
root / dir
for root in old_spotdl_dirs()
for dir in [".spotipy", "spotipy"]
]
)
)


def get_temp_path() -> Path:
def get_spotify_cache_path() -> Path:
"""
Get the path to the temp folder.
Get the path to the spotify cache folder.

### Returns
- The path to the temp folder.
- The path to the spotipy cache file.
"""

temp_path = get_spotdl_path() / "temp"
if not temp_path.exists():
os.mkdir(temp_path)

return temp_path
return get_path_fallback(
*(
[
root / dir
for root in [pd.user_cache_path(appname="spotdl")]
for dir in ["spotify_cache", ".spotify_cache"]
]
+ [
root / dir
for root in old_spotdl_dirs()
for dir in [".spotify_cache", "spotify_cache"]
]
)
)


def get_errors_path() -> Path:
Expand All @@ -143,12 +230,13 @@ def get_errors_path() -> Path:
- If the errors directory does not exist, it will be created.
"""

errors_path = get_spotdl_path() / "errors"

if not errors_path.exists():
os.mkdir(errors_path)

return errors_path
return get_path_fallback(
*(
root / dir
for root in [pd.user_log_path(appname="spotdl")] + old_spotdl_dirs()
for dir in ["errors"]
)
)


def get_web_ui_path() -> Path:
Expand All @@ -162,14 +250,14 @@ def get_web_ui_path() -> Path:
- If the web-ui directory does not exist, it will be created.
"""

# web_ui_path = get_spotdl_path() / "web-ui"
# web_ui_path = get_spotdl_path() / "src" / "spotdl" / "web" / "static"
web_ui_path = Path(__file__).parent.parent / "web" / "static"

if not web_ui_path.exists():
os.mkdir(web_ui_path)

return web_ui_path
return get_path_fallback(
pd.user_data_path(appname="spotdl") / "web" / "static",
*(
root / dir
for root in old_spotdl_dirs()
for dir in ["web-ui", Path("src") / "spotdl" / "web" / "static"]
)
)


def get_config() -> Dict[str, Any]:
Expand Down Expand Up @@ -312,7 +400,7 @@ def get_parameter(cls, key):
"auth_token": None,
"user_auth": False,
"headless": False,
"cache_path": str(get_cache_path()),
"cache_path": str(get_spotipy_client_cache_path()),
"no_cache": False,
"max_retries": 3,
"use_cache_file": False,
Expand Down
Loading