Compare commits

...

18 commits
pydub ... main

Author SHA1 Message Date
Asaril
2f72ab1cc1 add example systemd unit 2023-05-24 22:54:36 +02:00
Patrick Moessler
52f76b97ec add readme content 2023-05-23 01:56:32 +02:00
Patrick Moessler
988d4194be add random selection from starts play mode 2023-05-22 02:32:16 +02:00
Patrick Moessler
1c5113b74a more sensible default config 2023-05-22 02:13:29 +02:00
Patrick Moessler
4596d11200 add overwrite feature 2023-05-22 01:08:22 +02:00
Patrick Moessler
474e8f5773 add name to title config file 2023-05-21 23:04:54 +02:00
Patrick Moessler
4e863aca65 write / paths 2023-05-21 22:50:02 +02:00
Patrick Moessler
065c4bf389 add random_sequence play mode. fixes #3 2023-05-21 22:46:25 +02:00
Patrick Moessler
25dacc7216 add tool to quickly add tracklists 2023-05-21 22:46:24 +02:00
Patrick Moessler
2a5f2561d5 load config from media_path/tagid.json. fixes #4 2023-05-21 22:46:11 +02:00
Patrick Moessler
5b0695ec3e use tracklist from config. fixes #1, #2 2023-05-21 22:30:07 +02:00
Patrick Moessler
d089e11345 configure flake8 and fix issues 2023-05-21 22:28:11 +02:00
Patrick Moessler
4f8c27a529 allow lowercase log level input 2023-05-21 22:02:47 +02:00
a7dad655df Merge pull request 'mpd' (#5) from mpd into main
Reviewed-on: #5
2023-05-21 20:54:43 +02:00
Asaril
0e2e849483 allow flac 2023-05-21 00:24:00 +02:00
Asaril
ae91ed44c4 add mpd 2023-05-15 03:26:18 +02:00
Asaril
507c206f55 add dev deps 2023-05-15 02:00:16 +02:00
Asaril
bb88ba86ca remove mopidy 2023-05-15 01:59:59 +02:00
6 changed files with 604 additions and 232 deletions

View file

@ -0,0 +1,75 @@
# Sleepywaves
A RFID powered musick/audiobook player with configurable tracklists and playing modes.
Place a tag onto the reader to start playing. Remove it to stop, or configure a timeout to stop automatically.
## Prerequisites / Recommended Setup
- Raspberry Pi (any with audio and SPI. 3A+ works fine)
- MFRC522 RFID reader module
- storage medium, e.g. USB stick. Works with SD if nothing else is available.
- dietpi with the following packages installed:
- mpd - audio player
- python3
- pip3 install poetry
- (mympd) - optional web control UI
- (samba server) - optional file share server to easily copy new tracks
- enable SPI in dietpi-config
## Installation
Sleepywaves uses `poetry` to prepare the python environment. Install it with:
- Clone/download the repository
- Run `poetry install` to create a venv with the required python packages.
- See `sleepywaves.example.service` for a reference how a systemd unit file could look like
## Running
- Run `poetry shell` or use the python path from the poetry venv directly in the command below.
- Run `python sleepywaves.py -m <MEDIA DIR>`
Alternatively, start with systemd: `systemctl enable sleepywaves.service` and `systemctl start sleepywaves.service`.
## Tag / title configuration
A new config file is created in the media directory once a new tag is scanned.
It will be named `changeme_<tag id>.json`.
Remove the `changeme_` prefix, and configure the file as you wish.
Tracks are added to the `tracks` list in the file, and the start time in a track can be configured by
adding a `"<name>":<seconds>` pair to `start_times`.
Setting a timeout other than `0` will enable the sleep timer.
The player will stop after the configured time in seconds, even if the tag is still present on the reader.
These modes are supported:
| Play mode | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| sequence | Start with the first track (at custom start time if configured), advance through the list one-by-one |
| random | Select a random track from `tracks` (at custom start time if configured) each time the tag is put on the reader, or after one track finishes playing |
| random_sequence | Select a random track from `tracks` (at custom start time if configured) when the tag is put on the reader. After the track finishes, advance to the next in the `tracks` list |
| random_start_sequence | Select a random starting track and time from the list of `start_times`. Advance to the next track in `tracks` after it finishes playing |
Setting `resume_track` to true will save the current playing position if the tag is removed, or the timer is reached.
Playback will continue at this point when the tag is put on the reader again.
Note: if a custom `start_times` entry is present for a track, all playback will start at this time (i.e. even if the
track is next-in-line for sequence play mode).
Note: the `random_start_sequence` mode is most useful for using audiobooks with chapters broken into shorter tracks and
the sleep timer feature. If the book e.g. has tracks of ~4 minutes, but the timeout is set to 8 minutes, it will start
at one of the configured start tracks and continue playing across the track breaks, but never start at the intermediate
tracks.
### Adding tracks to a title easily
Adding tracks to the renamed file can be automated with the add_tracks.py tool.
Run it as `python add_tracks.py -m <media dir> -t <tag id> -p <file name pattern>`
A pattern of `*Hobbit*.mp3` will find `<media dir>/Hobbit/chapter1.mp3` and similar.
Using the `-o` flag will overwrite the track list in the title config file (*USE WITH CAUTION*). If not given, tracks will be appended to the existing tracklist.

385
poetry.lock generated
View file

@ -1,112 +1,127 @@
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
name = "black"
version = "23.3.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
{file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
{file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
{file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
{file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
{file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
{file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
{file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
{file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
{file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
{file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
{file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
{file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
{file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "flake8"
version = "6.0.0"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.8.1"
files = [
{file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
{file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.10.0,<2.11.0"
pyflakes = ">=3.0.0,<3.1.0"
[[package]]
name = "flake8-pyproject"
version = "1.2.3"
description = "Flake8 plug-in loading the configuration from pyproject.toml"
category = "dev"
optional = false
python-versions = ">= 3.6"
files = [
{file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
{file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
{file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"},
]
[[package]]
name = "charset-normalizer"
version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
{file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
{file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
{file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
{file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
{file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
{file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
{file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
{file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
]
[package.dependencies]
Flake8 = ">=5"
TOMLi = {version = "*", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["pyTest", "pyTest-cov"]
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
files = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
@ -126,6 +141,117 @@ files = [
"RPi.GPIO" = "*"
spidev = "*"
[[package]]
name = "mypy"
version = "1.3.0"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"},
{file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"},
{file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"},
{file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"},
{file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"},
{file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"},
{file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"},
{file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"},
{file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"},
{file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"},
{file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"},
{file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"},
{file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"},
{file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"},
{file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"},
{file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"},
{file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"},
{file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"},
{file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"},
{file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"},
{file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"},
{file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"},
{file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"},
{file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"},
{file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"},
{file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
[[package]]
name = "pathspec"
version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
]
[[package]]
name = "platformdirs"
version = "3.5.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"},
{file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"},
]
[package.extras]
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pycodestyle"
version = "2.10.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
{file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
]
[[package]]
name = "pydantic"
version = "1.10.7"
@ -180,26 +306,31 @@ dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "requests"
version = "2.30.0"
description = "Python HTTP for Humans."
category = "main"
name = "pyflakes"
version = "3.0.1"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
files = [
{file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"},
{file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"},
{file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
{file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[[package]]
name = "python-mpd2"
version = "3.1.0"
description = "A Python MPD client library"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "python-mpd2-3.1.0.tar.gz", hash = "sha256:f33c2cdb0d6baa74a36724f38c1c4a099a7ce2c8ec4a2bb7192150a5855df476"},
{file = "python_mpd2-3.1.0-py2.py3-none-any.whl", hash = "sha256:c4d44a54e88a675f7301fdb11a1bd31165a6f51a664dd41e8137e92f7b02ebfb"},
]
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
twisted = ["Twisted"]
[[package]]
name = "rpi-gpio"
@ -229,6 +360,18 @@ files = [
{file = "spidev-3.6.tar.gz", hash = "sha256:14dbc37594a4aaef85403ab617985d3c3ef464d62bc9b769ef552db53701115b"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.5.0"
@ -241,25 +384,7 @@ files = [
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
]
[[package]]
name = "urllib3"
version = "2.0.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"},
{file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "69fa4cae8fcc407d68e6d0e028e333e36308c2572bf6c2490fdfd4a1e7cae4f0"
content-hash = "bbed92f250e97d898bbff9948aa49a22308abedc4f9f5dd165c94e9af1972bae"

View file

@ -7,12 +7,23 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9"
spidev = "^3.6"
mfrc522 = "^0.0.7"
spidev = { version = "^3.6", "platform" = "linux" }
mfrc522 = { version = "^0.0.7", platform = "linux" }
pydantic = "^1.10.7"
requests = "^2.30.0"
python-mpd2 = "^3.1.0"
[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
mypy = "^1.3.0"
flake8 = "^6.0.0"
flake8-pyproject = "^1.2.3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.flake8]
max-line-length = 88
select = ['C', 'E', 'F', 'W', 'B', 'B950']
extend-ignore = ['E203', 'E501', 'W503']

View file

@ -0,0 +1,13 @@
[Unit]
Description=Sleepywaves
After=network.target
[Service]
Type=simple
Restart=always
RestartSec=1
User=dietpi
ExecStart=/home/dietpi/.cache/pypoetry/virtualenvs/sleepywaves-py3.9/bin/python /home/dietpi/sleepywaves/src/sleepywaves.py -m /mnt/usbstick
[Install]
WantedBy=multi-user.target

121
src/add_tracks.py Normal file
View file

@ -0,0 +1,121 @@
"""add_tracks.py
Easily add tracks to a given tag config by scanning the directory tree with a pattern.
"""
import json
import fnmatch
import logging
import os
import os.path
from typing import Any, Dict, List, Optional
log = logging.getLogger(__name__ if __name__ != "__main__" else "add_tracks")
log_levels = (
"CRITICAL",
"ERROR",
"WARNING",
"INFO",
"DEBUG",
)
def test_dir(files: List[str], pattern: str) -> List[str]:
log.debug("trying:\n " + "\n ".join(files))
valid_files = fnmatch.filter(files, pattern)
if valid_files:
log.info("found:\n " + "\n ".join(valid_files))
return valid_files
def main(
media_path: str,
tag: str,
pattern: str,
recursive: bool = False,
overwrite: bool = False,
) -> None:
abs_media_path = os.path.abspath(media_path)
log.info(
f"Scanning {abs_media_path}{' recursively' if recursive else ''} for {pattern}"
)
valid_paths: List[str] = []
if not recursive:
valid_paths = test_dir(
files=os.listdir(os.path.abspath(media_path)), pattern=pattern
)
else:
for root, _, files in os.walk(os.path.abspath(media_path)):
valid_paths.extend(
test_dir(
files=[
os.path.relpath(os.path.join(root, f), abs_media_path)
for f in files
],
pattern=pattern,
)
)
config: Optional[Dict[str, Any]] = None
config_path = os.path.join(abs_media_path, f"{tag}.json")
with open(config_path, "rb") as jf:
config = json.load(jf)
if config is None:
raise FileNotFoundError(f"Config file could not be loaded for tag id: {tag}")
tracks: List[str] = config.get("tracks", []) if not overwrite else []
tracks.extend(sorted((f.replace("\\", "/") for f in valid_paths)))
config["tracks"] = tracks
with open(config_path, "w") as jf:
json.dump(config, jf, indent=2)
if __name__ == "__main__":
from argparse import ArgumentParser
parser = ArgumentParser(description=__doc__)
parser.add_argument(
"-m",
"--media_path",
required=True,
help="path to the media directory (e.g., an USB stick)",
)
parser.add_argument(
"--log",
choices=list(log_levels) + [lv.lower() for lv in log_levels],
default="WARNING",
help="log level",
)
parser.add_argument("-t", "--tag", required=True, help="Tag ID to add tracks to.")
parser.add_argument("-p", "--pattern", required=True, help="Pattern to use")
parser.add_argument(
"-r",
"--recursive",
action="store_true",
help="Search recursively. Pattern applies to whole path (relative to media_path).",
)
parser.add_argument(
"-o",
"--overwrite",
action="store_true",
help="Overwrite and replace existing track list.",
)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
log.setLevel(args.log.upper())
main(
media_path=args.media_path,
tag=args.tag,
pattern=args.pattern,
recursive=args.recursive,
overwrite=args.overwrite,
)

View file

@ -2,21 +2,19 @@
RFID/NFC-based sleep time audio player. """
from typing import Literal, Optional, Dict, Any, List, Union
from typing import Literal, Optional, Dict, List
from abc import ABC, abstractmethod
import logging
import os
import os.path
import requests
import time
import urllib.parse
from dataclasses import dataclass
from mfrc522.SimpleMFRC522 import SimpleMFRC522
from pydantic import BaseModel
from random import randrange
from random import choice, randrange
from mpd import MPDClient
import RPi.GPIO as GPIO
@ -29,22 +27,25 @@ log_levels = (
"DEBUG",
)
PlayMode = Literal["random", "sequence"]
PlayMode = Literal["random", "sequence", "random_sequence", "random_start_sequence"]
CONFIG_FILE_NAME = "config.json"
NEW_CONFIG_FILE_NAME = "changeme_" + CONFIG_FILE_NAME
STATUS_FILE_NAME = "status.json"
NEW_CONFIG_FILE_PREFIX = "changeme_"
STATUS_FILE_SUFFIX = "_status"
class Config(BaseModel):
default_mode: PlayMode = "sequence"
default_timeout: int = 8 * 60
default_timeout: int = 0
class TitleConfig(BaseModel):
name: Optional[str] = None
mode: Optional[PlayMode] = None
timeout: Optional[int] = None
resume_track: Optional[bool] = True
tracks: Optional[List[str]] = None
start_times: Optional[Dict[str, int]] = None
class TitleStatus(BaseModel):
@ -57,14 +58,9 @@ class Title:
tag_id: int
config: TitleConfig
status: TitleStatus
tracks: list[str]
class Renderer(ABC):
@abstractmethod
def get_tracks(self, path: str) -> list[str]:
pass
@abstractmethod
def play(self, path: str, from_time: int) -> None:
pass
@ -81,6 +77,10 @@ class Renderer(ABC):
def is_playing(self) -> bool:
pass
@abstractmethod
def close(self) -> None:
pass
class Player:
reader: SimpleMFRC522
@ -110,19 +110,17 @@ class Player:
log.debug(f"loading global config from: {cfg_path}")
self.config = Config.parse_file(cfg_path)
def get_title_path(self, tag_id: int) -> str:
return os.path.join(self.media_path, f"{tag_id:10x}")
def get_title_file(self, tag_id: int, prefix: str = "", suffix: str = "") -> str:
return os.path.join(self.media_path, f"{prefix}{tag_id:010x}{suffix}.json")
def get_title_config(self, tag_id: int) -> Optional[TitleConfig]:
title_path = self.get_title_path(tag_id)
cfg_path = os.path.join(title_path, CONFIG_FILE_NAME)
cfg_path = self.get_title_file(tag_id=tag_id)
if os.path.exists(cfg_path):
return TitleConfig.parse_file(cfg_path)
return None
def get_title_status(self, tag_id: int) -> TitleStatus:
title_path = self.get_title_path(tag_id)
status_path = os.path.join(title_path, STATUS_FILE_NAME)
status_path = self.get_title_file(tag_id=tag_id, suffix=STATUS_FILE_SUFFIX)
if os.path.exists(status_path):
log.debug(f"loading title status from {status_path}")
return TitleStatus.parse_file(status_path)
@ -130,13 +128,17 @@ class Player:
return TitleStatus(last_track="", last_time=0)
def create_title(self, tag_id: int) -> None:
title_path = self.get_title_path(tag_id)
if not os.path.exists(title_path):
log.info(f"new title created: {title_path}")
os.makedirs(title_path)
new_cfg_path = os.path.join(title_path, NEW_CONFIG_FILE_NAME)
cfg_path = self.get_title_file(tag_id=tag_id)
new_cfg_path = self.get_title_file(tag_id=tag_id, prefix=NEW_CONFIG_FILE_PREFIX)
if not os.path.exists(cfg_path) and not os.path.exists(new_cfg_path):
log.info(f"new title created: {new_cfg_path}")
new_config = TitleConfig(
mode=self.config.default_mode, timeout=self.config.default_timeout
name="",
mode=self.config.default_mode,
timeout=self.config.default_timeout,
tracks=[],
start_times={},
resume_track=True,
)
with open(new_cfg_path, "w") as cfg_file:
cfg_file.write(new_config.json(indent=2))
@ -144,31 +146,58 @@ class Player:
def read_burst(self) -> Optional[int]:
tag_id: Optional[int] = None
for _ in range(10):
tag_id = self.reader.read_id_no_block() # type:ignore
tag_id = self.reader.read_id_no_block()
if tag_id is not None:
return tag_id
return None
def advance(self) -> None:
def advance(self, first: bool = False) -> None:
assert self.current_title is not None
assert self.current_title.config.tracks is not None
track_id = (
self.current_title.tracks.index(self.current_title.status.last_track)
self.current_title.config.tracks.index(self.current_title.status.last_track)
if self.current_title.status.last_track
else -1
)
log.debug(f"advance from #{track_id}: {self.current_title.status.last_track}")
if self.current_title.config.mode == "sequence":
track_id = (track_id + 1) % len(self.current_title.tracks)
track_id = (track_id + 1) % len(self.current_title.config.tracks)
elif self.current_title.config.mode == "random":
track_id = randrange(0, len(self.current_title.tracks), 1)
self.current_title.status.last_track = self.current_title.tracks[track_id]
track_id = randrange(0, len(self.current_title.config.tracks), 1)
elif self.current_title.config.mode == "random_sequence":
if first:
track_id = randrange(0, len(self.current_title.config.tracks), 1)
else:
track_id = (track_id + 1) % len(self.current_title.config.tracks)
elif self.current_title.config.mode == "random_start_sequence":
if first:
if not self.current_title.config.start_times:
track_id = 0
else:
start_name = choice(
list(self.current_title.config.start_times.keys())
)
track_id = self.current_title.config.tracks.index(start_name)
else:
track_id = (track_id + 1) % len(self.current_title.config.tracks)
self.current_title.status.last_track = self.current_title.config.tracks[
track_id
]
self.current_title.status.last_time = 0
if (
self.current_title.config.start_times is not None
and self.current_title.status.last_track
in self.current_title.config.start_times
):
self.current_title.status.last_time = self.current_title.config.start_times[
self.current_title.status.last_track
]
log.debug(f"advance to #{track_id}: {self.current_title.status.last_track}")
def start_playing(self) -> None:
if self.current_title is not None:
track_path = os.path.join(
self.get_title_path(self.current_title.tag_id),
self.media_path,
self.current_title.status.last_track,
)
log.debug(
@ -189,17 +218,16 @@ class Player:
)
self.renderer.stop()
status_path = os.path.join(
self.get_title_path(self.current_title.tag_id), STATUS_FILE_NAME
status_path = self.get_title_file(
tag_id=self.current_title.tag_id, suffix=STATUS_FILE_SUFFIX
)
with open(status_path, "w") as status_file:
status_file.write(self.current_title.status.json(indent=2))
self.current_title = None
def process(self):
def process(self) -> None:
tag_id: Optional[int] = self.read_burst()
# current_tag = self.current_title.tag_id if self.current_title else None
if tag_id != self.old_tag:
log.debug(f"self.old_tag: {self.old_tag or 0:10x} tag_id:{tag_id or 0:10x}")
if self.old_tag is not None:
@ -221,7 +249,7 @@ class Player:
self.advance()
self.start_playing()
def tag_detected(self, tag_id: int):
def tag_detected(self, tag_id: int) -> None:
log.info(f"tag detected: {tag_id:x}")
cfg = self.get_title_config(tag_id)
if not cfg:
@ -231,13 +259,12 @@ class Player:
tag_id=tag_id,
config=cfg,
status=self.get_title_status(tag_id),
tracks=self.renderer.get_tracks(self.get_title_path(tag_id)),
)
if (
self.current_title.status.last_track == ""
or self.current_title.config.resume_track is False
):
self.advance()
self.advance(first=True)
self.start_playing()
if (
self.current_title.config.timeout is not None
@ -245,11 +272,14 @@ class Player:
):
self.start_time = time.time()
def tag_removed(self):
def tag_removed(self) -> None:
if self.current_title is not None:
log.info(f"tag removed: {self.current_title.tag_id:x}")
self.stop_playing()
def close(self) -> None:
self.renderer.close()
class DebugRenderer(Renderer):
def __init__(self) -> None:
@ -277,75 +307,69 @@ class DebugRenderer(Renderer):
def is_playing(self) -> bool:
return (self.start != 0) and (self.get_time() - self.offset < 10)
def close(self) -> None:
log.debug("DebugRenderer: close()")
class MopidyRenderer(Renderer):
class MpdRenderer(Renderer):
def __init__(self) -> None:
super().__init__()
self._id = 1
self._post("core.tracklist.clear")
self._post("core.tracklist.set_consume", [True])
self._post("core.tracklist.set_single", [True])
def get_tracks(self, path: str) -> list[str]:
all_files = os.listdir(path)
return sorted([f for f in all_files if f.endswith(".mp3")])
self.mpd: MPDClient = MPDClient()
self.mpd.timeout = 10
self.mpd.connect("/var/run/mpd/socket")
self.mpd.clear()
self.mpd.single(1)
self.mpd.consume(1)
# self.start: int = 0
# self.offset: int = 0
def play(self, path: str, from_time: int) -> None:
log.info(f"MopidyRenderer: play({path}, {from_time})")
quoted_path = urllib.parse.quote(path)
uri = f"file://{quoted_path}"
added_tracks = self._post("core.tracklist.add", {"uris": [uri]})
tlid = added_tracks[0]["tlid"]
self._post("core.playback.play", {"tlid": tlid})
if from_time > 0:
playing = self.is_playing()
while not playing:
time.sleep(0.1)
playing = self.is_playing()
seeked = False
while not seeked:
time.sleep(0.1)
seeked = self._post(
"core.playback.seek", {"time_position": from_time * 1000}
)
log.info(f"MpdRenderer: play({path}, {from_time})")
self.mpd.clear()
self.mpd.add(f"file://{path}")
self.mpd.play()
if from_time != 0:
self.mpd.seekcur(from_time)
# self.start = int(time.time())
# self.offset = from_time
def get_time(self) -> int:
res = self._post("core.playback.get_time_position")
return int(res / 1000)
# return int(time.time()) - self.start + self.offset
return int(float(self.mpd.status()["elapsed"]))
def stop(self) -> None:
log.info("MopidyRenderer: stop()")
self._post("core.playback.stop")
self._post("core.tracklist.clear")
log.info("MpdRenderer: stop()")
self.mpd.stop()
self.mpd.clear()
# self.start = 0
# self.offset = 0
def is_playing(self) -> bool:
res = self._post("core.playback.get_state")
return res == "playing"
return self.mpd.status()["state"] == "play"
# return (self.start != 0) and (self.get_time() - self.offset < 10)
def _post(
self, method: str, data: Union[Dict[str, Any], List[Any], None] = None
) -> Any:
msg: Dict[str, Any] = {"jsonrpc": "2.0", "id": self._id, "method": method}
if data is not None:
msg["params"] = data
log.debug(f"MopidyRenderer POST: {msg}")
response = requests.post("http://localhost:6680/mopidy/rpc", json=msg).json()
log.debug(f"MopidyRenderer POST response: {response}")
self._id += 1
return response["result"]
def close(self) -> None:
self.mpd.close()
self.mpd.disconnect()
def main(media_path: str, renderer_type: str) -> None:
if renderer_type == "dummy":
renderer = DebugRenderer()
elif renderer_type == "mopidy":
renderer = MopidyRenderer()
elif renderer_type == "mpd":
renderer = MpdRenderer()
else:
renderer = DebugRenderer()
player = Player(media_path=media_path, renderer=renderer)
log.info("sleepywaves ready.")
try:
while True:
player.process()
time.sleep(1)
finally:
player.close()
if __name__ == "__main__":
@ -359,19 +383,22 @@ if __name__ == "__main__":
help="path to the media directory (e.g., an USB stick)",
)
parser.add_argument(
"--log", choices=log_levels, default="WARNING", help="log level"
"--log",
choices=list(log_levels) + [lv.lower() for lv in log_levels],
default="WARNING",
help="log level",
)
parser.add_argument(
"--renderer",
choices=("dummy", "mopidy"),
default="mopidy",
choices=("dummy", "mpd"),
default="mpd",
help="media renderer to use as backend",
)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
log.setLevel(args.log)
log.setLevel(args.log.upper())
try:
main(media_path=args.media_path, renderer_type=args.renderer)