Each playlist now has its own station checkboxes in Settings.
Sync scrapes the union of all stations once, then filters songs
by source before syncing each playlist separately.
Config stored as PLAYLISTS JSON: [{name, stations}, ...].
Backward-compatible with flat PLAYLIST_NAME + SELECTED_STATIONS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
5.0 KiB
Python
173 lines
5.0 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from journeyfm.paths import data_path
|
|
|
|
try:
|
|
import keyring
|
|
except Exception:
|
|
keyring = None
|
|
|
|
CONFIG_PATH = data_path("config.json")
|
|
KEYRING_SERVICE = "JourneyFMPlaylistCreator"
|
|
DEFAULT_CONFIG = {
|
|
"PLEX_TOKEN": "",
|
|
"SERVER_IP": "",
|
|
"PLAYLIST_NAME": "Journey FM Recently Played",
|
|
"AUTO_UPDATE": False,
|
|
"UPDATE_INTERVAL": 15,
|
|
"UPDATE_UNIT": "Minutes",
|
|
"SELECTED_STATIONS": ["journey_fm", "spirit_fm"],
|
|
}
|
|
NON_SECRET_KEYS = {
|
|
"SERVER_IP",
|
|
"PLAYLIST_NAME",
|
|
"PLAYLIST_NAMES",
|
|
"PLAYLISTS",
|
|
"AUTO_UPDATE",
|
|
"UPDATE_INTERVAL",
|
|
"UPDATE_UNIT",
|
|
"SELECTED_STATIONS",
|
|
}
|
|
|
|
|
|
def is_containerized():
|
|
return os.path.exists("/.dockerenv") or os.getenv("JOURNEYFM_CONTAINER") == "1"
|
|
|
|
|
|
def _normalize_selected_stations(value):
|
|
if isinstance(value, str):
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
if isinstance(value, list):
|
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
return list(DEFAULT_CONFIG["SELECTED_STATIONS"])
|
|
|
|
|
|
def _read_config_file(config_path=CONFIG_PATH):
|
|
if not Path(config_path).exists():
|
|
return {}
|
|
try:
|
|
with open(config_path, "r", encoding="utf-8") as file_handle:
|
|
return json.load(file_handle)
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _write_config_file(config, config_path=CONFIG_PATH):
|
|
with open(config_path, "w", encoding="utf-8") as file_handle:
|
|
json.dump(config, file_handle, indent=2)
|
|
|
|
|
|
def get_secret(key):
|
|
env_value = os.getenv(key, "").strip()
|
|
if env_value:
|
|
return env_value
|
|
if not is_containerized() and keyring is not None:
|
|
try:
|
|
stored = keyring.get_password(KEYRING_SERVICE, key)
|
|
if stored:
|
|
return stored
|
|
except Exception:
|
|
pass
|
|
file_config = _read_config_file()
|
|
return str(file_config.get(key, "")).strip()
|
|
|
|
|
|
def set_secret(key, value):
|
|
if not value:
|
|
delete_secret(key)
|
|
return
|
|
if not is_containerized() and keyring is not None:
|
|
try:
|
|
keyring.set_password(KEYRING_SERVICE, key, value)
|
|
return
|
|
except Exception:
|
|
pass
|
|
file_config = _read_config_file()
|
|
file_config[key] = value
|
|
_write_config_file(file_config)
|
|
|
|
|
|
def delete_secret(key):
|
|
if not is_containerized() and keyring is not None:
|
|
try:
|
|
keyring.delete_password(KEYRING_SERVICE, key)
|
|
except Exception:
|
|
pass
|
|
file_config = _read_config_file()
|
|
if key in file_config:
|
|
del file_config[key]
|
|
_write_config_file(file_config)
|
|
|
|
|
|
def migrate_legacy_secrets(config_path=CONFIG_PATH):
|
|
file_config = _read_config_file(config_path)
|
|
token = str(file_config.get("PLEX_TOKEN", "")).strip()
|
|
if token and not is_containerized() and keyring is not None:
|
|
try:
|
|
keyring.set_password(KEYRING_SERVICE, "PLEX_TOKEN", token)
|
|
del file_config["PLEX_TOKEN"]
|
|
_write_config_file(file_config, config_path)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def load_runtime_config(config_path=CONFIG_PATH):
|
|
migrate_legacy_secrets(config_path)
|
|
config = dict(DEFAULT_CONFIG)
|
|
file_config = _read_config_file(config_path)
|
|
config.update({key: value for key, value in file_config.items() if key in NON_SECRET_KEYS})
|
|
config["PLEX_TOKEN"] = get_secret("PLEX_TOKEN")
|
|
|
|
env_server = os.getenv("SERVER_IP", "").strip()
|
|
env_playlist = os.getenv("PLAYLIST_NAME", "").strip()
|
|
env_stations = os.getenv("SELECTED_STATIONS", "").strip()
|
|
env_auto_update = os.getenv("AUTO_UPDATE", "").strip()
|
|
env_interval = os.getenv("UPDATE_INTERVAL", "").strip()
|
|
env_unit = os.getenv("UPDATE_UNIT", "").strip()
|
|
|
|
if env_server:
|
|
config["SERVER_IP"] = env_server
|
|
if env_playlist:
|
|
config["PLAYLIST_NAME"] = env_playlist
|
|
if env_stations:
|
|
config["SELECTED_STATIONS"] = _normalize_selected_stations(env_stations)
|
|
else:
|
|
config["SELECTED_STATIONS"] = _normalize_selected_stations(config.get("SELECTED_STATIONS"))
|
|
if env_auto_update:
|
|
config["AUTO_UPDATE"] = env_auto_update.lower() in {"1", "true", "yes", "on"}
|
|
if env_interval:
|
|
try:
|
|
config["UPDATE_INTERVAL"] = int(env_interval)
|
|
except ValueError:
|
|
pass
|
|
if env_unit:
|
|
config["UPDATE_UNIT"] = env_unit
|
|
|
|
return config
|
|
|
|
|
|
def save_runtime_config(config, config_path=CONFIG_PATH):
|
|
config = dict(config)
|
|
token = str(config.pop("PLEX_TOKEN", "")).strip()
|
|
if token:
|
|
set_secret("PLEX_TOKEN", token)
|
|
|
|
persisted = {}
|
|
for key in NON_SECRET_KEYS:
|
|
if key not in config:
|
|
continue
|
|
value = config[key]
|
|
if isinstance(value, list):
|
|
persisted[key] = ",".join(value)
|
|
else:
|
|
persisted[key] = value
|
|
_write_config_file(persisted, config_path)
|
|
|
|
|
|
def get_display_config(config_path=CONFIG_PATH):
|
|
config = load_runtime_config(config_path)
|
|
config["PLEX_TOKEN"] = get_secret("PLEX_TOKEN")
|
|
return config
|