Skip to content

Commit

Permalink
Merge pull request #24 from Wilhelmsson177/main
Browse files Browse the repository at this point in the history
feat: Store Settings and CLI
  • Loading branch information
alexhartm authored Mar 1, 2024
2 parents 10602aa + 505303f commit edcfe13
Show file tree
Hide file tree
Showing 15 changed files with 1,296 additions and 957 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ dmypy.json

# Other
*.code-workspace
*.yaml
test.py
todo.md
!.pre-commit-config.yaml

# vscode
.vscode
# Ignore dynaconf secret files
.secrets.*
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10.11
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
],
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
1,924 changes: 1,004 additions & 920 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ repository = "https://github.com/alexhartm/tonie-podcast-sync"
readme = "README.md"
license = "MIT"
keywords = ["toniebox", "podcast"]
packages = [{ include = "toniepodcastsync.py" }, { include = "podcast.py" }]
packages = [{ include = "tonie_podcast_sync" }]

[tool.poetry.dependencies]
python = "^3.10.11"
feedparser = "^6.0.10"
tonie-api = "^0.1.1"
rich = "^13.5.2"
ruff = "^0.1.13"
black = "^23.12.1"
pre-commit = "^3.6.0"
pathvalidate = "^3.2.0"
pydub = "^0.25.1"
python-slugify = "^8.0.1"
dynaconf = "^3.2.3"
typer = { extras = ["all"], version = "^0.9.0" }
tomli-w = "^1.0.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-mock = "^3.11.1"
ruff = "^0.1.13"
black = "^23.12.1"
pre-commit = "^3.6.0"
responses = "^0.23.3"

[tool.poetry.scripts]
tonie-podcast-sync = "tonie_podcast_sync.cli:app"

[tool.ruff]
# Add "Q" to the list of enabled codes.
select = ["ALL"]
Expand Down
5 changes: 4 additions & 1 deletion rpi_example/tps.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"""example python script for using tonie-podcast-sync."""
# ruff: noqa: ERA001, INP001

from toniepodcastsync import EpisodeSorting, Podcast, ToniePodcastSync

from tonie_podcast_sync.toniepodcastsync import Podcast, ToniePodcastSync

maus = Podcast("https://kinder.wdr.de/radio/diemaus/audio/gute-nacht-mit-der-maus/diemaus-gute-nacht-104.podcast")
pumuckl = Podcast("https://feeds.br.de/pumuckl/feed.xml")
Expand All @@ -11,6 +12,8 @@

tps = ToniePodcastSync("address-used-for-toniecloud@your-mailprovider.com", "toniecloud-password")

# uncomment the following lines if you want to print out
# a list of all your creative-tonies and their IDs
# uncomment the following lines if you want to print out
# a list of all your creative-tonies and their IDs
# and then exit this script:
Expand Down
File renamed without changes.
31 changes: 31 additions & 0 deletions tests/res/responses.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
responses:
- response:
auto_calculate_content_length: false
body: "ID3\x03\0\0\0\0\x04WTDAT\0\0\0\v\0\0\x01\uFFFD\uFFFD2\02\00\08\0TYER\0\0\
\0\v\0\0\x01\uFFFD\uFFFD2\00\02\03\0TLAN\0\0\0\t\0\0\x01\uFFFD\uFFFDD\0E\0U\0\
\04\uFFFD\0\0\x04UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
content_type: text/plain; charset=utf-8
method: GET
status: 200
url: https://podcast-mp3.dradio.de/podcast/2023/08/22/muss_ich_mich_vor_hexen_fuerchten_neu_drk_20230822_1520_80d53d3b.mp3?refId=kakadu-104
- response:
auto_calculate_content_length: false
body: "ID3\x03\0\0\0\0\x06!TDAT\0\0\0\v\0\0\x01\uFFFD\uFFFD1\07\00\08\0TYER\0\0\
\0\v\0\0\x01\uFFFD\uFFFD2\00\02\03\0TLAN\0\0\0\t\0\0\x01\uFFFD\uFFFDD\0E\0U\0\
\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\
\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD"
content_type: text/plain; charset=utf-8
method: GET
status: 200
url: https://podcast-mp3.dradio.de/podcast/2023/08/17/kakadu_update_17082023_wie_entstehen_steine_kopfweh_durch_drk_20230817_1400_b618758c.mp3?refId=kakadu-104
- response:
auto_calculate_content_length: false
body: "ID3\x03\0\0\0\0\x05\aTDAT\0\0\0\v\0\0\x01\uFFFD\uFFFD1\05\00\08\0TYER\0\
\0\0\v\0\0\x01\uFFFD\uFFFD2\00\02\03\0TLAN\0\0\0\t\0\0\x01\uFFFD\uFFFDD\0E\0\
U\0TALB\0\0\0\x0F\0\0\x01\uFFFD\uFFFDK\0A\0K\0A\0D\0U\0TIT2\0\0\0m\0\0\x01\uFFFD\
\uFFFD\uFFFDd@\uFFFD\uFFFD\0\0i\0\0\0\b\0\0\r \0\0\x01\0\0\x01\uFFFD\0\0\0 \0\
\04\uFFFD\0\0\x04UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
content_type: text/plain; charset=utf-8
method: GET
status: 200
url: https://podcast-mp3.dradio.de/podcast/2023/08/15/kakadu_podcast_warum_spielen_wir_voe_am_15082023_drk_20230815_1400_26ceac12.mp3?refId=kakadu-104
2 changes: 1 addition & 1 deletion tests/test_podcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from podcast import EpisodeSorting, Podcast
from tonie_podcast_sync.podcast import EpisodeSorting, Podcast


def test_url_type():
Expand Down
71 changes: 51 additions & 20 deletions tests/test_podcast_sync.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import datetime
import locale
import platform
import textwrap
from pathlib import Path
from unittest import mock

import pytest
import responses
from tonie_api.models import Chapter, CreativeTonie, Household

from podcast import Podcast
from toniepodcastsync import ToniePodcastSync
from tonie_podcast_sync.podcast import Podcast
from tonie_podcast_sync.toniepodcastsync import ToniePodcastSync

locale.setlocale(locale.LC_TIME, "en_US") # is only set for consistent tests

HOUSEHOLD = Household(id="1234", name="My House", ownerName="John", access="owner", canLeave=True)
CHAPTER_1 = Chapter(id="chap-1", title="The great chapter", file="123456789A", seconds=4711, transcoding=False)
Expand Down Expand Up @@ -40,6 +45,32 @@
chapters=[CHAPTER_1, CHAPTER_2],
)

WINDOWS_RESULT = [
" List of all creative tonies. ",
"┌────┬───────────────┬──────────────────────┬───────────┬─────────────────────┐",
"│ ID │ Name of Tonie │ Time of last update │ Household │ Latest Episode name │",
"├────┼───────────────┼──────────────────────┼───────────┼─────────────────────┤",
"│ 42 │ Tonie #1 │ │ My House │ No latest chapter │",
"│ │ │ │ │ available. │",
"│ 73 │ Tonie #2 │ 11/25/2016 12:00:00 │ My House │ The great chapter │",
"│ │ │ PM │ │ │",
"└────┴───────────────┴──────────────────────┴───────────┴─────────────────────┘",
"",
]

LINUX_RESULT = [
" List of all creative tonies. ",
"┏━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓",
"┃ ID ┃ Name of Tonie ┃ Time of last update ┃ Household ┃ Latest Episode name ┃",
"┡━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩",
"│ 42 │ Tonie #1 │ │ My House │ No latest chapter │",
"│ │ │ │ │ available. │",
"│ 73 │ Tonie #2 │ 11/25/2016 12:00:00 │ My House │ The great chapter │",
"│ │ │ PM │ │ │",
"└────┴───────────────┴──────────────────────┴───────────┴──────────────────────┘",
"",
]


def _get_tonie_api_mock() -> mock.MagicMock:
tonie_api_mock = mock.MagicMock()
Expand All @@ -52,7 +83,7 @@ def _get_tonie_api_mock() -> mock.MagicMock:

@pytest.fixture()
def mocked_tonie_api():
with mock.patch("toniepodcastsync.TonieAPI") as _mock:
with mock.patch("tonie_podcast_sync.toniepodcastsync.TonieAPI") as _mock:
yield _mock


Expand All @@ -63,28 +94,28 @@ def mocked_responses():
yield rsps


def test_show_overview(mocked_tonie_api: mock.Mock, capfd: pytest.CaptureFixture):
@pytest.fixture()
def overview_result():
result = []
match platform.system():
case "Windows":
result = WINDOWS_RESULT
case "Linux":
result = LINUX_RESULT
case _unknown:
raise NotImplementedError(_unknown)
return textwrap.dedent("\n".join(result))


def test_show_overview(mocked_tonie_api: mock.Mock, capfd: pytest.CaptureFixture, overview_result):
tonie_api_mock = _get_tonie_api_mock()
mocked_tonie_api.return_value = tonie_api_mock
tps = ToniePodcastSync("some user", "some_pass")
tps.print_tonies_overview()
mocked_tonie_api.assert_called_once_with("some user", "some_pass")
tonie_api_mock.get_households.assert_called_once()
captured = capfd.readouterr()
assert captured.out == "\n".join(
[
" List of all creative tonies. ",
"┏━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓",
"┃ ID ┃ Name of Tonie ┃ Time of last update ┃ Household ┃ Latest Episode name ┃",
"┡━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩",
"│ 42 │ Tonie #1 │ │ My House │ No latest chapter │",
"│ │ │ │ │ available. │",
"│ 73 │ Tonie #2 │ Fri Nov 25 12:00:00 │ My House │ The great chapter │",
"│ │ │ 2016 │ │ │",
"└────┴───────────────┴──────────────────────┴───────────┴──────────────────────┘",
"",
],
)
assert captured.out == overview_result


def test_upload_podcast(mocked_tonie_api: mock.Mock, mocked_responses: responses.RequestsMock):
Expand All @@ -97,6 +128,6 @@ def test_upload_podcast(mocked_tonie_api: mock.Mock, mocked_responses: responses
TONIE_1,
Path("podcasts")
/ "Kakadu - Der Kinderpodcast"
/ "Wed, 23 Aug 2023 03:00:15 +0200 Vorurteile - Muss ich mich vor Hexen fürchten?.mp3",
"Vorurteile - Muss ich mich vor Hexen fürchten? (Wed, 23 Aug 2023 03:00:15 +0200)",
/ "mon-14-aug-2023-10-35-24-0200_vom-gewinnen-und-verlieren-warum-spielen-wir-so-gern.mp3",
"Vom Gewinnen und Verlieren - Warum spielen wir so gern? (Mon, 14 Aug 2023 10:35:24 +0200)",
)
Empty file added tonie_podcast_sync/__init__.py
Empty file.
156 changes: 156 additions & 0 deletions tonie_podcast_sync/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""The command line interface module for the tonie-podcast-sync."""

from pathlib import Path

import tomli_w
from dynaconf.vendor.box.exceptions import BoxError
from rich.console import Console
from rich.prompt import Confirm, IntPrompt, Prompt
from tonie_api.models import CreativeTonie
from typer import Typer

from tonie_podcast_sync.config import APP_SETTINGS_DIR, settings
from tonie_podcast_sync.podcast import EpisodeSorting, Podcast
from tonie_podcast_sync.toniepodcastsync import MAXIMUM_TONIE_MINUTES, ToniePodcastSync

app = Typer(pretty_exceptions_show_locals=False)


@app.command()
def update_tonies() -> None:
"""Update the tonies by using the settings file."""
try:
tps = ToniePodcastSync(settings.TONIE_CLOUD_ACCESS.USERNAME, settings.TONIE_CLOUD_ACCESS.PASSWORD)
except BoxError:
Console().print(
"There was an error getting the username or password. Please create the settings file or setting the",
"environment variables. TPS_TONIE_CLOUD_ACCESS_USERNAME and TPS_TONIE_CLOUD_ACCESS_PASSWORD.",
)
return
for ct_key, ct_value in settings.CREATIVE_TONIES.items():
tps.sync_podcast_to_tonie(
Podcast(
ct_value.podcast,
episode_sorting=ct_value.episode_sorting,
volume_adjustment=ct_value.volume_adjustment,
episode_min_duration_sec=ct_value.episode_min_duration_sec,
),
ct_key,
ct_value.maximum_length,
)


@app.command()
def create_settings_file() -> None:
"""Create a settings file in your user home."""
keep_secrets = False
if Path(APP_SETTINGS_DIR / ".secrets.toml").exists():
keep_secrets = Confirm.ask("You already have secrets set, do you want to keep them?")

if not keep_secrets:
user_name = Prompt.ask("Enter your Tonie CloudAPI username")
password = Prompt.ask("Enter your password for Tonie CloudAPI", password=True)
save_login = Confirm.ask("Do you want to save your login data in a .secrets.toml file")

if save_login:
APP_SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
with Path(APP_SETTINGS_DIR / ".secrets.toml").open("wb") as _fs:
tomli_w.dump({"tonie_cloud_access": {"username": user_name, "password": password}}, _fs)
else:
user_name, password = settings.TONIE_CLOUD_ACCESS.USERNAME, settings.TONIE_CLOUD_ACCESS.PASSWORD
try:
tps = ToniePodcastSync(user=user_name, pwd=password)
except KeyError:
Console().print("It seems like you are not able to login, please provide different login data.")
return
tonies = tps.get_tonies()
data = {}

for tonie in tonies:
podcast = Prompt.ask(
f"Which podcast do you want to set for Tonie {tonie.name} with ID {tonie.id}?\n"
"Please enter the URL to the podcast, or leave empty if you don't want to set it.",
)
if podcast:
data[tonie.id] = {"podcast": podcast, "name": tonie.name}
else:
continue
_ask_episode_order(data, tonie)
_ask_maximum_tonie_length(data, tonie)
_ask_minimum_episode_length(data, tonie)
_ask_volume_adjustment(data, tonie)

with Path(APP_SETTINGS_DIR / "settings.toml").open("wb") as _fs:
tomli_w.dump({"creative_tonies": data}, _fs)


def _ask_episode_order(data: dict, tonie: CreativeTonie) -> None:
episode_order_input = Prompt.ask(
"How would you like your podcast episodes sorted?",
choices=list(EpisodeSorting),
default=EpisodeSorting.BY_DATE_NEWEST_FIRST,
)
data[tonie.id]["episode_sorting"] = episode_order_input


def _ask_maximum_tonie_length(data: dict, tonie: CreativeTonie) -> None:
maximum_length_input = IntPrompt.ask(
"What should be the maximum length of the podcast?\n"
f"Defaults to the maximum of {MAXIMUM_TONIE_MINUTES} minutes.",
default=90,
)
match maximum_length_input:
case None:
data[tonie.id]["maximum_length"] = MAXIMUM_TONIE_MINUTES
case maximum_length if 0 < maximum_length <= MAXIMUM_TONIE_MINUTES:
data[tonie.id]["maximum_length"] = maximum_length_input
case maximum_length if maximum_length <= 0 or maximum_length > MAXIMUM_TONIE_MINUTES:
Console().print(
"The value you have entered is out of range."
f"Will be set to default value of {MAXIMUM_TONIE_MINUTES}.",
)
data[tonie.id]["maximum_length"] = MAXIMUM_TONIE_MINUTES


def _ask_minimum_episode_length(data: dict, tonie: CreativeTonie) -> None:
minimum_length_input = IntPrompt.ask(
"What should be the minimum length (in sec) of the podcast?\n"
"Defaults to the minimum of 0 seconds.\n"
"Podcasts shorter than the input, will not be uploaded.",
default=0,
)
match minimum_length_input:
case None:
data[tonie.id]["episode_min_duration_sec"] = 0
case x if x < 0:
Console().print("The value you have set, is less than 0 and will be set to 0.")
data[tonie.id]["episode_min_duration_sec"] = 0
case x if x > 60 * data[tonie.id]["maximum_length"]:
Console().print(
"The value you have set, is less more than the maximum available length for the tonie."
"It will be set to the maximum, but probably no Episode will be downloaded.",
)
data[tonie.id]["episode_min_duration_sec"] = 60 * data[tonie.id]["maximum_length"]
case _:
data[tonie.id]["episode_min_duration_sec"] = minimum_length_input


def _ask_volume_adjustment(data: dict, tonie: CreativeTonie) -> None:
volume_adjustment_input = IntPrompt.ask(
"Would you like to adjust the volume of the Episodes?\n"
"If set, the downloaded audio will be adjusted by the given amount in dB.\n"
"Defaults to 0, i.e. no adjustment",
default=0,
)
match volume_adjustment_input:
case None:
data[tonie.id]["volume_adjustment"] = 0
case x if x < 0:
Console().print("The value you have set, is less than 0 and will be set to 0.")
data[tonie.id]["volume_adjustment"] = 0
case _:
data[tonie.id]["volume_adjustment"] = volume_adjustment_input


if __name__ == "__main__":
app()
Loading

0 comments on commit edcfe13

Please sign in to comment.