Compare commits
11 Commits
webapi-las
...
more-front
| Author | SHA1 | Date | |
|---|---|---|---|
| e5a2f7bc78 | |||
| 735167db22 | |||
| 11e31aabc2 | |||
| 32e996e446 | |||
| 49197c8ca4 | |||
| b20a31ccf4 | |||
| 8a2d621c7d | |||
| e447902001 | |||
| 93ea5036dc | |||
| aee5a48967 | |||
| 936020df58 |
@@ -11,8 +11,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize submodules
|
||||
run: cd software && ./update-submodules.sh
|
||||
- name: Prepare venv
|
||||
run: python -m venv build-venv && source build-venv/bin/activate && pip install freezefs
|
||||
- name: Build
|
||||
run: cd software && ./build.sh
|
||||
run: source build-venv/bin/activate && cd software && ./build.sh
|
||||
- name: Upload firmware
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -22,3 +22,5 @@ module("mp3player.py", "../../src")
|
||||
module("webserver.py", "../../src")
|
||||
package("utils", base_path="../../src")
|
||||
package("nfc", base_path="../../src")
|
||||
|
||||
module("frozen_frontend.py", "../../build")
|
||||
|
||||
@@ -25,6 +25,11 @@ mkdir "$FS_STAGE_DIR"/fs
|
||||
trap 'rm -rf $FS_STAGE_DIR' EXIT
|
||||
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/fs "$FS_STAGE_DIR"/filesystem.bin
|
||||
|
||||
FRONTEND_STAGE_DIR=$(mktemp -d)
|
||||
trap 'rm -rf $FRONTEND_STAGE_DIR' EXIT
|
||||
gzip -c frontend/index.html > "$FRONTEND_STAGE_DIR"/index.html.gz
|
||||
python -m freezefs "$FRONTEND_STAGE_DIR" build/frozen_frontend.py --target=/frontend
|
||||
|
||||
for hwconfig in boards/RPI_PICO_W/manifest-*.py; do
|
||||
hwconfig_base=$(basename "$hwconfig")
|
||||
hwname=${hwconfig_base##manifest-}
|
||||
|
||||
450
software/frontend/index.html
Normal file
450
software/frontend/index.html
Normal file
@@ -0,0 +1,450 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Device Admin</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
nav button {
|
||||
margin: 5px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Screens */
|
||||
.screen {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.screen.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Config editor UI */
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
display: block;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.nested {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #ddd;
|
||||
padding-left: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Tree view */
|
||||
.tree ul {
|
||||
list-style: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.tree li {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.caret {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.caret::before {
|
||||
content: "▶";
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
li.expanded > .caret::before {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
li:not(:has(ul)) > .caret::before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.node {
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.node.selected {
|
||||
background: #0078d7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tree ul ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li.expanded > ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scroll-tree-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Device Admin</h1>
|
||||
|
||||
<nav>
|
||||
<button onclick="showScreen('menu')">🏠 Main Menu</button>
|
||||
<button onclick="showScreen('config')">⚙️ Config Editor</button>
|
||||
<button onclick="showScreen('playlist')">🖹 Playlist Editor</button>
|
||||
</nav>
|
||||
|
||||
<!-- MAIN MENU -->
|
||||
<div id="screen-menu" class="screen active">
|
||||
<h2>Main Menu</h2>
|
||||
<p>Select a tool:</p>
|
||||
|
||||
<ul>
|
||||
<li><button onclick="showScreen('config')">Open Config Editor</button></li>
|
||||
<li><button onclick="showScreen('playlist')">Open Playlist Editor</button></li>
|
||||
<!-- More screens can be added later -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- CONFIG EDITOR SCREEN -->
|
||||
<div id="screen-config" class="screen">
|
||||
<h2>Configuration Editor</h2>
|
||||
<div id="config-container">Loading…</div>
|
||||
<button id="config-save-btn" disabled>Save</button>
|
||||
</div>
|
||||
|
||||
<!-- PLAYLIST EDITOR SCREEN -->
|
||||
<div id="screen-playlist" class="screen">
|
||||
<h2>Playlist Editor</h2>
|
||||
<div id="playlist-container">Loading…</div>
|
||||
<div class="scroll-tree-container">
|
||||
<div class="tree" id="tree">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Fruits</span>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Apple</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Citrus</span>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Orange</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Lemon</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="caret"></span>
|
||||
<span class="node">Strawberry</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button id="playlist-save-btn" disabled>Save</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const Screens = {};
|
||||
let activeScreen = null;
|
||||
/* -----------------------------
|
||||
Screen switching
|
||||
----------------------------- */
|
||||
function showScreen(name) {
|
||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById('screen-' + name).classList.add('active');
|
||||
|
||||
activeScreen = name;
|
||||
Screens[name]?.onShow?.();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
CONFIG EDITOR LOGIC
|
||||
----------------------------- */
|
||||
Screens.config = (() => {
|
||||
let screenroot = null;
|
||||
function init() {
|
||||
screenroot = document.getElementById('screen-config');
|
||||
document.getElementById('config-save-btn').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/v1/config');
|
||||
const original = await res.json();
|
||||
const updated = serialize(original);
|
||||
|
||||
try {
|
||||
const saveRes = await fetch('/api/v1/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updated, null, 2)
|
||||
});
|
||||
|
||||
if (!saveRes.ok) {
|
||||
alert("Failed to save config: " + await saveRes.text());
|
||||
return;
|
||||
}
|
||||
|
||||
alert("Configuration saved successfully!");
|
||||
} catch (err) {
|
||||
alert("Error saving configuration: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onShow() {
|
||||
const container = document.getElementById('config-container');
|
||||
container.innerHTML = "Loading…";
|
||||
document.getElementById('config-save-btn').disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/config');
|
||||
const config = await res.json();
|
||||
render(config);
|
||||
} catch (err) {
|
||||
container.innerHTML = "Failed to load config: " + err;
|
||||
}
|
||||
}
|
||||
|
||||
function render(config) {
|
||||
const container = document.getElementById('config-container');
|
||||
container.innerHTML = "";
|
||||
container.appendChild(renderObject(config, "root"));
|
||||
document.getElementById('config-save-btn').disabled = false;
|
||||
}
|
||||
|
||||
const config_names = {
|
||||
'root.IDLE_TIMEOUT_SECS': 'Idle Timeout (seconds)',
|
||||
'root.BUTTON_MAP': 'Button map',
|
||||
'root.BUTTON_MAP.NEXT': 'Next track',
|
||||
'root.BUTTON_MAP.PREV': 'Previous track',
|
||||
'root.BUTTON_MAP.VOLUP': 'Volume up',
|
||||
'root.BUTTON_MAP.VOLDOWN': 'Volume down',
|
||||
'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause',
|
||||
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
|
||||
'root.TAGMODE': 'Tag mode',
|
||||
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain'
|
||||
};
|
||||
const config_input_override = {
|
||||
'root.TAGMODE': {
|
||||
'element': 'select',
|
||||
'values': {
|
||||
'tagremains': 'Play until tag is removed',
|
||||
'tagstartstop': 'Present tag once to start, present again to stop playback'
|
||||
}
|
||||
},
|
||||
'root.IDLE_TIMEOUT_SECS': {
|
||||
'input-type': 'number'
|
||||
},
|
||||
'root.TAG_TIMEOUT_SECS': {
|
||||
'input-type': 'number'
|
||||
},
|
||||
'root.LED_COUNT': {
|
||||
'input-type': 'number'
|
||||
},
|
||||
};
|
||||
|
||||
function renderObject(obj, path) {
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
const currentPath = path + '.' + key;
|
||||
const label = document.createElement('label');
|
||||
if (currentPath in config_names) {
|
||||
label.textContent = config_names[currentPath];
|
||||
} else {
|
||||
label.textContent = key;
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
wrapper.appendChild(label);
|
||||
const nested = document.createElement('div');
|
||||
nested.className = "nested";
|
||||
nested.appendChild(renderObject(value, currentPath));
|
||||
wrapper.appendChild(nested);
|
||||
} else {
|
||||
wrapper.appendChild(label);
|
||||
if (currentPath in config_input_override && 'element' in config_input_override[currentPath]) {
|
||||
const override = config_input_override[currentPath];
|
||||
if (override['element'] === 'select') {
|
||||
const input = document.createElement('select');
|
||||
input.dataset.path = currentPath;
|
||||
|
||||
for (const val in override.values) {
|
||||
const option = document.createElement('option');
|
||||
option.value = val;
|
||||
option.textContent = override.values[val];
|
||||
if (val === value) {
|
||||
option.selected = true;
|
||||
}
|
||||
|
||||
input.appendChild(option);
|
||||
}
|
||||
|
||||
wrapper.appendChild(input);
|
||||
}
|
||||
} else {
|
||||
const input = document.createElement('input');
|
||||
if (currentPath in config_input_override && 'input-type' in config_input_override[currentPath]) {
|
||||
input.type = config_input_override[currentPath]['input-type'];
|
||||
}
|
||||
input.value = value === null ? "" : value;
|
||||
input.dataset.path = currentPath;
|
||||
|
||||
wrapper.appendChild(input);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function serialize(rootObj) {
|
||||
const inputs = screenroot.querySelectorAll("input[data-path], select[data-path]");
|
||||
|
||||
inputs.forEach(input => {
|
||||
const path = input.dataset.path.split('.').slice(1); // remove "root"
|
||||
let current = rootObj;
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
current = current[path[i]];
|
||||
}
|
||||
|
||||
let val = input.value.trim();
|
||||
if (val === "") val = null;
|
||||
else if (!isNaN(val)) val = Number(val);
|
||||
|
||||
current[path[path.length - 1]] = val;
|
||||
});
|
||||
|
||||
return rootObj;
|
||||
}
|
||||
|
||||
return { init, onShow };
|
||||
})();
|
||||
|
||||
/* -----------------------------
|
||||
PLAYLIST EDITOR LOGIC
|
||||
----------------------------- */
|
||||
Screens.playlist = (() => {
|
||||
let lastSelected = null;
|
||||
|
||||
function init() {
|
||||
document.getElementById("tree").addEventListener("click", (e) => {
|
||||
// CARET CLICK → expand/collapse
|
||||
const caret = e.target.closest(".caret");
|
||||
if (caret) {
|
||||
const li = caret.parentElement;
|
||||
if (li.querySelector("ul")) {
|
||||
li.classList.toggle("expanded");
|
||||
}
|
||||
return; // IMPORTANT: don't affect selection
|
||||
}
|
||||
|
||||
// NODE LABEL CLICK → selection only
|
||||
const node = e.target.closest(".node");
|
||||
if (!node) return;
|
||||
|
||||
if (e.shiftKey && lastSelected) {
|
||||
selectRange(lastSelected, node);
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
node.classList.toggle("selected");
|
||||
} else {
|
||||
clearSelection();
|
||||
node.classList.add("selected");
|
||||
}
|
||||
|
||||
lastSelected = node;
|
||||
});
|
||||
}
|
||||
|
||||
async function onShow() {
|
||||
const container = document.getElementById('playlist-container');
|
||||
container.innerHTML = "Loading…";
|
||||
document.getElementById('playlist-save-btn').disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/playlist');
|
||||
const config = await res.json();
|
||||
renderPlaylistForm(config);
|
||||
} catch (err) {
|
||||
container.innerHTML = "Failed to load config: " + err;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlaylistForm(config) {
|
||||
const container = document.getElementById('playlist-container');
|
||||
container.innerHTML = "";
|
||||
container.appendChild(renderPlaylistObject(config, "root"));
|
||||
document.getElementById('playlist-save-btn').disabled = false;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
tree.querySelectorAll(".selected").forEach(n =>
|
||||
n.classList.remove("selected")
|
||||
);
|
||||
}
|
||||
|
||||
function selectRange(start, end) {
|
||||
const nodes = [...tree.querySelectorAll(".node")];
|
||||
const startIndex = nodes.indexOf(start);
|
||||
const endIndex = nodes.indexOf(end);
|
||||
|
||||
const [from, to] = startIndex < endIndex
|
||||
? [startIndex, endIndex]
|
||||
: [endIndex, startIndex];
|
||||
|
||||
clearSelection();
|
||||
nodes.slice(from, to + 1).forEach(n =>
|
||||
n.classList.add("selected")
|
||||
);
|
||||
}
|
||||
|
||||
return { init, onShow };
|
||||
})();
|
||||
|
||||
// Initialization
|
||||
Object.values(Screens).forEach(screen => {
|
||||
screen.init?.();
|
||||
});
|
||||
|
||||
showScreen("menu");
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
software/requirements.txt
Normal file
1
software/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
freezefs
|
||||
@@ -51,7 +51,7 @@ class PlayerApp:
|
||||
self.playlist_db = deps.playlistdb(self)
|
||||
self.hwconfig = deps.hwconfig(self)
|
||||
self.leds = deps.leds(self)
|
||||
self.tag_mode = self.playlist_db.getSetting('tagmode')
|
||||
self.tag_mode = self.config.get_tagmode()
|
||||
self.playing_tag = None
|
||||
self.playlist = None
|
||||
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||
|
||||
@@ -14,6 +14,7 @@ import ubinascii
|
||||
# Own modules
|
||||
import app
|
||||
from audiocore import AudioContext
|
||||
import frozen_frontend # noqa: F401
|
||||
from mfrc522 import MFRC522
|
||||
from mp3player import MP3Player
|
||||
from nfc import Nfc
|
||||
|
||||
@@ -21,7 +21,8 @@ class Configuration:
|
||||
'VOLDOWN': 2,
|
||||
'PREV': None,
|
||||
'NEXT': 1,
|
||||
}
|
||||
},
|
||||
'TAGMODE': 'tagremains'
|
||||
}
|
||||
|
||||
def __init__(self, config_path='/config.json'):
|
||||
@@ -29,6 +30,7 @@ class Configuration:
|
||||
try:
|
||||
with open(self.config_path, 'r') as conf_file:
|
||||
self.config = json.load(conf_file)
|
||||
self._merge_configs(self.DEFAULT_CONFIG, self.config)
|
||||
except OSError as ex:
|
||||
if ex.errno == ENOENT:
|
||||
self.config = Configuration.DEFAULT_CONFIG
|
||||
@@ -50,6 +52,16 @@ class Configuration:
|
||||
raise
|
||||
os.sync()
|
||||
|
||||
def _merge_configs(self, default, config):
|
||||
for k in default.keys():
|
||||
if k not in config:
|
||||
if isinstance(default[k], dict):
|
||||
config[k] = default[k].copy()
|
||||
else:
|
||||
config[k] = default[k]
|
||||
elif isinstance(default[k], dict):
|
||||
self._merge_configs(default[k], config[k])
|
||||
|
||||
def _save(self):
|
||||
with open(self.config_path + '.new', 'w') as conf_file:
|
||||
json.dump(self.config, conf_file)
|
||||
@@ -58,7 +70,7 @@ class Configuration:
|
||||
os.sync()
|
||||
|
||||
def _get(self, key):
|
||||
return self.config.get(key, self.DEFAULT_CONFIG[key])
|
||||
return self.config[key]
|
||||
|
||||
def get_led_count(self) -> int:
|
||||
return self._get('LED_COUNT')
|
||||
@@ -72,6 +84,9 @@ class Configuration:
|
||||
def get_button_map(self) -> Mapping[str, int | None]:
|
||||
return self._get('BUTTON_MAP')
|
||||
|
||||
def get_tagmode(self) -> str:
|
||||
return self._get('TAGMODE')
|
||||
|
||||
# For the web API
|
||||
def get_config(self) -> Mapping[str, Any]:
|
||||
return self.config
|
||||
@@ -87,5 +102,8 @@ class Configuration:
|
||||
|
||||
def set_config(self, config):
|
||||
self._validate(self.DEFAULT_CONFIG, config)
|
||||
if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']:
|
||||
raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'")
|
||||
self._merge_configs(self.config, config)
|
||||
self.config = config
|
||||
self._save()
|
||||
|
||||
@@ -31,9 +31,6 @@ class BTreeDB(IPlaylistDB):
|
||||
PERSIST_NO = b'no'
|
||||
PERSIST_TRACK = b'track'
|
||||
PERSIST_OFFSET = b'offset'
|
||||
DEFAULT_SETTINGS = {
|
||||
b'tagmode': b'tagremains'
|
||||
}
|
||||
|
||||
class Playlist(IPlaylist):
|
||||
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle):
|
||||
@@ -282,11 +279,6 @@ class BTreeDB(IPlaylistDB):
|
||||
self._savePlaylist(tag, entries, persist, shuffle)
|
||||
return self.getPlaylistForTag(tag)
|
||||
|
||||
def getSetting(self, key: bytes | str) -> str:
|
||||
if type(key) is str:
|
||||
key = key.encode()
|
||||
return self.db.get(b'settings/' + key, self.DEFAULT_SETTINGS[key]).decode()
|
||||
|
||||
def validate(self, dump=False):
|
||||
"""
|
||||
Validate the structure of the playlist database.
|
||||
@@ -306,8 +298,7 @@ class BTreeDB(IPlaylistDB):
|
||||
fail(f'Malformed key {k!r}')
|
||||
continue
|
||||
if fields[0] == b'settings':
|
||||
val = self.db[k].decode()
|
||||
print(f'Setting {fields[1].decode()} = {val}')
|
||||
# Legacy, not used any more
|
||||
continue
|
||||
if last_tag != fields[0]:
|
||||
last_tag = fields[0]
|
||||
|
||||
@@ -5,7 +5,7 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
||||
|
||||
import asyncio
|
||||
|
||||
from microdot import Microdot
|
||||
from microdot import Microdot, redirect, send_file
|
||||
|
||||
webapp = Microdot()
|
||||
server = None
|
||||
@@ -29,7 +29,7 @@ async def before_request_handler(request):
|
||||
app.reset_idle_timeout()
|
||||
|
||||
|
||||
@webapp.route('/')
|
||||
@webapp.route('/api/v1/hello')
|
||||
async def index(request):
|
||||
print("wohoo, a guest :)")
|
||||
print(f" app: {request.app}")
|
||||
@@ -72,3 +72,21 @@ async def config_put(request):
|
||||
async def last_tag_uid_get(request):
|
||||
tag, _ = nfc.get_last_uid()
|
||||
return {'tag': tag}
|
||||
|
||||
|
||||
@webapp.route('/', methods=['GET'])
|
||||
async def root_get(request):
|
||||
return redirect('/index.html')
|
||||
|
||||
|
||||
@webapp.route('/index.html', methods=['GET'])
|
||||
async def index_get(request):
|
||||
return send_file('/frontend/index.html.gz', content_type='text/html', compressed='gzip')
|
||||
|
||||
|
||||
@webapp.route('/static/<path:path>', methods=['GET'])
|
||||
async def static(request, path):
|
||||
if '..' in path:
|
||||
# directory traversal is not allowed
|
||||
return 'Not found', 404
|
||||
return send_file('/frontend/static/' + path, max_age=86400)
|
||||
|
||||
@@ -136,6 +136,9 @@ class FakeConfig:
|
||||
def get_tag_timeout(self):
|
||||
return 5
|
||||
|
||||
def get_tagmode(self):
|
||||
return 'tagremains'
|
||||
|
||||
|
||||
def fake_open(filename, mode):
|
||||
return FakeFile(filename, mode)
|
||||
@@ -226,18 +229,16 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
|
||||
|
||||
|
||||
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
||||
class MyFakePlaylistDb(FakePlaylistDb):
|
||||
def __init__(self, tracklist=[b'test/path.mp3']):
|
||||
super().__init__(tracklist)
|
||||
class FakeStartStopConfig(FakeConfig):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
if key == 'tagmode':
|
||||
return 'tagstartstop'
|
||||
return None
|
||||
def get_tagmode(self):
|
||||
return 'tagstartstop'
|
||||
|
||||
fake_db = MyFakePlaylistDb()
|
||||
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db, config=FakeStartStopConfig)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
@@ -264,16 +265,7 @@ def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
||||
|
||||
|
||||
def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
|
||||
class MyFakePlaylistDb(FakePlaylistDb):
|
||||
def __init__(self, tracklist=[b'test/path.mp3']):
|
||||
super().__init__(tracklist)
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
if key == 'tagmode':
|
||||
return 'tagremains'
|
||||
return None
|
||||
|
||||
fake_db = MyFakePlaylistDb()
|
||||
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||
app.PlayerApp(deps)
|
||||
|
||||
Reference in New Issue
Block a user