Radioserver na Raspberry PI: webová část

Zobrazení: 13

Webová část rádioserveru v Nette: správa stanic a nahrávání MP tři přes formulář, automatická tvorba m3u playlistu a restart stanice přes sudoers a systemd.

Ilustrace webové správy rádia: Raspberry Pi server a dashboard v Nette pro stanice, nahrávání mp3 souborů, generování m3u playlistu a restart stanice. Vytvořil Chat GPT

Toto je navazující díl k článku o serverové části rádia. Tam jsme postavili Icecast, Liquidsoap a systemd jednotky tak, aby stačilo mít playlisty .m3u v jednom adresáři a wrapper služba je rozjela. V tomto díle uděláme webové UI v Nette, které bude stanice spravovat: vytvoří stanici, nahraje MP3 soubory přes formulář, udrží tabulku tracků, vygeneruje slug.m3u a provede restart dané stanice.

Kompatibilita s prvním dílem: pořád platí, že server čeká playlisty jako /var/lib/radio/<slug>.m3u a podle nich startuje instance radio@<slug>.service (nebo wrapper restartuje všechny). Jen už je nevytváříš ručně, ale vytváří je web.


Co budeme stavět

  • Sekci aplikace App\UI\Radio a presenter RadioPresenter.
  • Tabulku stanic (radio_stations) a tabulku tracků (radio_tracks).
  • Pohledy (Latte): seznam stanic, vytvoření, editace, detail stanice (upload + tracky + restart).
  • Upload MP3 přes webový formulář, uložení do /var/lib/radio/uploads/<slug>.
  • Generování playlistu /var/lib/radio/<slug>.m3u (absolutní cesty na MP3).
  • Restart stanice přes sudoers a systemctl restart radio@<slug>.service.

Bezpečnostní zásady

  • Webová část je admin sekce: ověřujeme přihlášení a roli admin.
  • Web nebude mít plná práva roota. Dostane jen vybrané příkazy přes sudoers.
  • Upload přijme jen MP3 (kontrola přípony), soubor se uloží pod bezpečným názvem.

Adresáře a práva na serveru

Budeme držet adresáře kompatibilní s prvním dílem:

  • /var/lib/radio – výsledné playlisty <slug>.m3u
  • /var/lib/radio/uploads/<slug> – nahrané MP3 soubory dané stanice

OCOL příkaz, který vše připraví (včetně setgid bitu, aby skupina zůstávala www-data):

sudo bash -lc '
set -e
LOG=/tmp/radio_web_01_dirs_perms.log
exec > >(tee "$LOG") 2>&1

mkdir -p /var/lib/radio /var/lib/radio/uploads

chown -R root:www-data /var/lib/radio
chmod 2775 /var/lib/radio
chmod 2775 /var/lib/radio/uploads

echo "== perms =="
ls -ld /var/lib/radio /var/lib/radio/uploads

echo "DONE. LOG: $LOG"
'

Sudoers: povolíme restart stanice pro www-data

Web poběží jako www-data, takže mu povolíme restart instance radio@SLUG.service (a volitelně wrapper radio.service).

sudo bash -lc '
set -e
LOG=/tmp/radio_web_02_sudoers.log
exec > >(tee "$LOG") 2>&1

cat > /etc/sudoers.d/fomixy-radio <<"EOF"
Defaults:www-data !requiretty

Cmnd_Alias FOMIXY_RADIO = \
	/bin/systemctl restart radio@*.service, \
	/bin/systemctl start radio@*.service, \
	/bin/systemctl stop radio@*.service, \
	/bin/systemctl restart radio.service, \
	/bin/systemctl start radio.service, \
	/bin/systemctl stop radio.service

www-data ALL=(root) NOPASSWD: FOMIXY_RADIO
EOF

chmod 0440 /etc/sudoers.d/fomixy-radio
visudo -cf /etc/sudoers.d/fomixy-radio

echo "DONE. LOG: $LOG"
'

Databáze: tabulky pro stanice a tracky

-- radio_stations
CREATE TABLE radio_stations (
	id INT AUTO_INCREMENT PRIMARY KEY,
	name VARCHAR(255) NOT NULL,
	slug VARCHAR(255) NOT NULL,
	description TEXT NULL,
	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
	UNIQUE KEY uniq_radio_stations_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- radio_tracks
CREATE TABLE radio_tracks (
	id INT AUTO_INCREMENT PRIMARY KEY,
	station_id INT NOT NULL,
	original_name VARCHAR(255) NOT NULL,
	stored_name VARCHAR(255) NOT NULL,
	abs_path TEXT NOT NULL,
	title VARCHAR(255) NULL,
	position INT NULL,
	uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
	CONSTRAINT fk_radio_tracks_station
		FOREIGN KEY (station_id) REFERENCES radio_stations(id)
		ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE INDEX idx_radio_tracks_station_pos
	ON radio_tracks(station_id, position, id);

Nette struktura

  • app/UI/Radio/RadioPresenter.php
  • app/UI/Radio/templates/Radio/default.latte – seznam stanic
  • app/UI/Radio/templates/Radio/create.latte – vytvoření stanice
  • app/UI/Radio/templates/Radio/edit.latte – editace stanice
  • app/UI/Radio/templates/Radio/show.latte – detail stanice + upload + tracky + restart

Celý soubor: RadioPresenter.php

Poznámka: metody jsou odsazené tabulátorem.

<?php
declare(strict_types=1);

namespace App\UI\Radio;

use App\UI\BasePresenter;
use Nette\Application\UI\Form;
use Nette\Database\Explorer;
use Nette\Database\Table\ActiveRow;
use Nette\Http\IResponse;
use Nette\Utils\Strings;
use Nette\Utils\FileSystem;

final class RadioPresenter extends BasePresenter
{
	private Explorer $db;

	private ?ActiveRow $station = null;

	private const RADIO_M3U_DIR = '/var/lib/radio';
	private const RADIO_UPLOAD_DIR = '/var/lib/radio/uploads';
	private const SYSTEMD_BIN = '/bin/systemctl';

	public function __construct(Explorer $db)
	{
		parent::__construct();
		$this->db = $db;
	}

	private function requireAdmin(): void
	{
		$user = $this->getUser();
		if (!$user->isLoggedIn() || !$user->isInRole('admin')) {
			$this->flashMessage($this->translator->translate('insufficient.rights'), 'error');
			$this->redirect(':Admin:SignIn');
			$this->error('Access denied', IResponse::S403_FORBIDDEN);
		}
	}

	public function renderDefault(): void
	{
		$this->requireAdmin();

		$this->template->pageTitle = 'Rádio – stanice';
		$this->template->stations = $this->db->table('radio_stations')->order('name');
	}

	public function actionCreate(): void
	{
		$this->requireAdmin();
		$this->template->pageTitle = 'Vytvořit stanici';
	}

	public function renderCreate(): void
	{
		$this->requireAdmin();
		$this->template->pageTitle = 'Vytvořit stanici';
	}

	public function actionEdit(int $id): void
	{
		$this->requireAdmin();

		$station = $this->db->table('radio_stations')->get($id);
		if (!$station) {
			$this->flashMessage('Stanice nebyla nalezena.', 'danger');
			$this->redirect('Radio:default');
		}

		$this->station = $station;
		$this->template->station = $station;
	}

	public function renderEdit(int $id): void
	{
		$this->requireAdmin();

		if ($this->station === null) {
			$this->station = $this->db->table('radio_stations')->get($id);
			$this->template->station = $this->station;
		}

		if ($this->station !== null) {
			$station = $this->station;

			$this->template->pageTitle = 'Upravit stanici – ' . $station->name;

			$form = $this['stationForm'];
			$form->setDefaults([
				'name' => $station->name,
				'slug' => $station->slug,
				'description' => $station->description,
			]);
		}
	}

	public function actionShow(string $slug): void
	{
		$this->requireAdmin();

		$station = $this->db->table('radio_stations')
			->where('slug', $slug)
			->fetch();

		if (!$station) {
			$this->flashMessage('Stanice nebyla nalezena.', 'danger');
			$this->redirect('Radio:default');
		}

		$this->station = $station;

		$tracks = $this->db->table('radio_tracks')
			->where('station_id', $station->id)
			->order('position IS NULL, position ASC, id ASC');

		$this->template->station = $station;
		$this->template->tracks = $tracks;

		$this->template->playlistPath = rtrim(self::RADIO_M3U_DIR, '/') . '/' . $station->slug . '.m3u';
	}

	public function renderShow(string $slug): void
	{
		$this->requireAdmin();
		$this->template->pageTitle = $this->station ? (string) $this->station->name : 'Rádio';
	}

	protected function createComponentStationForm(): Form
	{
		$form = new Form;

		$form->addText('name', 'Název stanice:')
			->setRequired('Zadej název stanice.');

		$form->addText('slug', 'URL adresa (slug):')
			->setRequired('Zadej slug stanice (bez mezer).');

		$form->addTextArea('description', 'Popis stanice:');

		$form->addSubmit('send', 'Uložit');

		$form->onSuccess[] = function (Form $form, \stdClass $values): void {
			$this->processStationForm($values);
		};

		return $form;
	}

	private function processStationForm(\stdClass $values): void
	{
		$id = $this->getParameter('id');
		$slug = $this->sanitizeSlug((string) $values->slug);

		if ($slug === '') {
			$this->flashMessage('Slug nesmí být prázdný.', 'danger');
			$this->redirect('this');
		}

		$slugRow = $this->db->table('radio_stations')
			->where('slug', $slug)
			->fetch();

		if ($id === null) {
			if ($slugRow) {
				$this->flashMessage('Tento slug už existuje. Zvol jiný.', 'danger');
				$this->redirect('this');
			}

			$row = $this->db->table('radio_stations')->insert([
				'name' => $values->name,
				'slug' => $slug,
				'description' => $values->description,
			]);

			$this->ensureUploadDirForStation($row);
			$this->writeStationM3u($row);

			$this->flashMessage('Stanice byla vytvořena.', 'success');
			$this->redirect('Radio:show', ['slug' => $row->slug]);

		} else {
			$station = $this->db->table('radio_stations')->get((int) $id);
			if (!$station) {
				$this->flashMessage('Stanice nebyla nalezena.', 'danger');
				$this->redirect('Radio:default');
			}

			if ($slugRow && (int) $slugRow->id !== (int) $station->id) {
				$this->flashMessage('Tento slug už existuje u jiné stanice. Zvol jiný.', 'danger');
				$this->redirect('this');
			}

			$oldSlug = (string) $station->slug;

			$station->update([
				'name' => $values->name,
				'slug' => $slug,
				'description' => $values->description,
			]);

			if ($oldSlug !== (string) $station->slug) {
				$this->renameStationUploadDir($oldSlug, (string) $station->slug);
				$this->deleteStationM3uBySlug($oldSlug);
			}

			$this->writeStationM3u($station);

			$this->flashMessage('Stanice byla upravena.', 'success');
			$this->redirect('Radio:show', ['slug' => $station->slug]);
		}
	}

	protected function createComponentUploadTrackForm(): Form
	{
		$form = new Form;

		$form->addHidden('station_id');

		$form->addUpload('file', 'MP3 soubor:')
			->setRequired('Vyber MP3 soubor.');

		$form->addText('title', 'Název skladby (volitelné):');

		$form->addSubmit('send', 'Nahrát do stanice');

		$form->onSuccess[] = function (Form $form, \stdClass $values): void {
			$this->processUploadTrackForm($values);
		};

		return $form;
	}

	private function processUploadTrackForm(\stdClass $values): void
	{
		$this->requireAdmin();

		$station = $this->db->table('radio_stations')->get((int) $values->station_id);
		if (!$station) {
			$this->flashMessage('Stanice nebyla nalezena.', 'danger');
			$this->redirect('Radio:default');
		}

		$file = $values->file;
		if (!$file->isOk()) {
			$this->flashMessage('Upload se nezdařil.', 'danger');
			$this->redirect('this');
		}

		$orig = (string) $file->getName();
		$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));

		if ($ext !== 'mp3') {
			$this->flashMessage('Povoleny jsou jen MP3 soubory.', 'danger');
			$this->redirect('this');
		}

		$this->ensureUploadDirForStation($station);

		$storedName = $this->safeFilename(pathinfo($orig, PATHINFO_FILENAME)) . '-' . date('Ymd-His') . '.mp3';
		$absDir = $this->getStationUploadDir((string) $station->slug);
		$absPath = $absDir . '/' . $storedName;

		$file->move($absPath);
		@chmod($absPath, 0664);

		$maxPosition = $this->db->table('radio_tracks')
			->where('station_id', $station->id)
			->max('position');

		$nextPosition = $maxPosition !== null ? ((int) $maxPosition + 1) : 1;

		$this->db->table('radio_tracks')->insert([
			'station_id' => $station->id,
			'original_name' => $orig,
			'stored_name' => $storedName,
			'abs_path' => $absPath,
			'title' => $values->title ? (string) $values->title : null,
			'position' => $nextPosition,
		]);

		$this->writeStationM3u($station);

		$ok = $this->restartStation((string) $station->slug);

		$this->flashMessage($ok
			? 'Skladba byla nahrána, playlist byl aktualizován a stanice byla restartována.'
			: 'Skladba byla nahrána a playlist byl aktualizován, ale restart stanice selhal.'
		, $ok ? 'success' : 'warning');

		$this->redirect('Radio:show', ['slug' => $station->slug]);
	}

	public function handleDeleteTrack(int $id): void
	{
		$this->requireAdmin();

		$track = $this->db->table('radio_tracks')->get($id);
		if (!$track) {
			$this->flashMessage('Track nebyl nalezen.', 'danger');
			$this->redirect('Radio:default');
		}

		$station = $this->db->table('radio_stations')->get((int) $track->station_id);
		if (!$station) {
			$this->flashMessage('Stanice nebyla nalezena.', 'danger');
			$this->redirect('Radio:default');
		}

		$path = (string) $track->abs_path;

		$track->delete();

		if ($path !== '' && is_file($path)) {
			@unlink($path);
		}

		$this->writeStationM3u($station);
		$ok = $this->restartStation((string) $station->slug);

		$this->flashMessage($ok
			? 'Skladba byla odstraněna a stanice byla restartována.'
			: 'Skladba byla odstraněna, ale restart stanice selhal.'
		, $ok ? 'success' : 'warning');

		$this->redirect('Radio:show', ['slug' => $station->slug]);
	}

	public function handleDeleteStation(int $id): void
	{
		$this->requireAdmin();

		$station = $this->db->table('radio_stations')->get($id);
		if (!$station) {
			$this->flashMessage('Stanice nebyla nalezena.', 'danger');
			$this->redirect('Radio:default');
		}

		$slug = (string) $station->slug;

		$dir = $this->getStationUploadDir($slug);
		if (is_dir($dir)) {
			FileSystem::delete($dir);
		}

		$station->delete();
		$this->deleteStationM3uBySlug($slug);

		$this->stopStation($slug);

		$this->flashMessage('Stanice byla smazána.', 'success');
		$this->redirect('Radio:default');
	}

	public function handleRestartStation(string $slug): void
	{
		$this->requireAdmin();

		$ok = $this->restartStation($slug);
		$this->flashMessage($ok ? 'Stanice byla restartována.' : 'Restart stanice selhal.', $ok ? 'success' : 'danger');
		$this->redirect('this');
	}

	private function writeStationM3u(ActiveRow $station): void
	{
		$slug = (string) $station->slug;
		$path = rtrim(self::RADIO_M3U_DIR, '/') . '/' . $slug . '.m3u';

		$tracks = $this->db->table('radio_tracks')
			->where('station_id', $station->id)
			->order('position IS NULL, position ASC, id ASC');

		$lines = ['#EXTM3U'];

		foreach ($tracks as $track) {
			$absPath = (string) $track->abs_path;
			if ($absPath === '' || !is_file($absPath)) {
				continue;
			}

			$title = trim((string) ($track->title ?: $track->original_name));
			$lines[] = '#EXTINF:-1,' . $title;
			$lines[] = $absPath;
		}

		if (count($lines) === 1) {
			@unlink($path);
			return;
		}

		$content = implode("\n", $lines) . "\n";
		$tmp = $path . '.tmp';

		$ok = file_put_contents($tmp, $content);
		if ($ok === false) {
			$err = error_get_last();
			$msg = $err['message'] ?? 'unknown';
			throw new \RuntimeException('Nelze zapsat M3U: ' . $tmp . ' (' . $msg . ')');
		}

		@chmod($tmp, 0664);

		if (!@rename($tmp, $path)) {
			@unlink($tmp);
			throw new \RuntimeException('Nelze přejmenovat M3U na: ' . $path);
		}
	}

	private function deleteStationM3uBySlug(string $slug): void
	{
		$path = rtrim(self::RADIO_M3U_DIR, '/') . '/' . $slug . '.m3u';
		if (is_file($path)) {
			@unlink($path);
		}
	}

	private function restartStation(string $slug): bool
	{
		$slug = $this->sanitizeSlug($slug);
		if ($slug === '') {
			return false;
		}

		$cmd = 'sudo ' . escapeshellarg(self::SYSTEMD_BIN) . ' restart ' . escapeshellarg('radio@' . $slug . '.service') . ' 2>&1';
		exec($cmd, $out, $code);
		return $code === 0;
	}

	private function stopStation(string $slug): bool
	{
		$slug = $this->sanitizeSlug($slug);
		if ($slug === '') {
			return false;
		}

		$cmd = 'sudo ' . escapeshellarg(self::SYSTEMD_BIN) . ' stop ' . escapeshellarg('radio@' . $slug . '.service') . ' 2>&1';
		exec($cmd, $out, $code);
		return $code === 0;
	}

	private function sanitizeSlug(string $s): string
	{
		$s = trim($s);
		$s = Strings::webalize($s, '-');
		return $s;
	}

	private function safeFilename(string $s): string
	{
		$s = trim($s);
		$s = Strings::toAscii($s);
		$s = preg_replace('/[^a-zA-Z0-9._-]+/', '-', $s) ?: '';
		$s = trim($s, '-');
		if ($s === '') {
			$s = 'track';
		}
		return $s;
	}

	private function getStationUploadDir(string $slug): string
	{
		return rtrim(self::RADIO_UPLOAD_DIR, '/') . '/' . $slug;
	}

	private function ensureUploadDirForStation(ActiveRow $station): void
	{
		$slug = (string) $station->slug;
		$dir = $this->getStationUploadDir($slug);

		if (!is_dir($dir)) {
			FileSystem::createDir($dir);
			@chmod($dir, 2775);
		}
	}

	private function renameStationUploadDir(string $oldSlug, string $newSlug): void
	{
		$oldDir = $this->getStationUploadDir($oldSlug);
		$newDir = $this->getStationUploadDir($newSlug);

		if (is_dir($oldDir) && !is_dir($newDir)) {
			@rename($oldDir, $newDir);
		}
	}
}

Pohledy (Latte šablony)

app/UI/Radio/templates/Radio/default.latte

{block content}
<h1>{$pageTitle}</h1>

<p>
	<a n:href="create" class="btn btn-primary">Vytvořit stanici</a>
</p>

<table class="table table-striped">
	<thead>
		<tr>
			<th>Název</th>
			<th>Slug</th>
			<th>Akce</th>
		</tr>
	</thead>
	<tbody>
		{foreach $stations as $s}
			<tr>
				<td>{$s->name}</td>
				<td>{$s->slug}</td>
				<td>
					<a n:href="show, slug => $s->slug" class="btn btn-sm btn-outline-primary">Detail</a>
					<a n:href="edit, id => $s->id" class="btn btn-sm btn-outline-secondary">Upravit</a>
				</td>
			</tr>
		{/foreach}
	</tbody>
</table>
{/block}

app/UI/Radio/templates/Radio/create.latte

{block content}
<h1>{$pageTitle}</h1>
{control stationForm}
{/block}

app/UI/Radio/templates/Radio/edit.latte

{block content}
<h1>{$pageTitle}</h1>
{control stationForm}
{/block}

app/UI/Radio/templates/Radio/show.latte

{block content}
<h1>{$pageTitle}</h1>

<p>
	<a n:href="default" class="btn btn-outline-secondary">Zpět na stanice</a>
	<a n:href="edit, id => $station->id" class="btn btn-outline-primary">Upravit</a>
</p>

<div class="card mb-3">
	<div class="card-body">
		<h5 class="card-title">Informace</h5>
		<p class="mb-1"><strong>Slug:</strong> {$station->slug}</p>
		{if $station->description}
			<p class="mb-1"><strong>Popis:</strong> {$station->description}</p>
		{/if}
		<p class="mb-0"><strong>Playlist:</strong> {$playlistPath}</p>
	</div>
</div>

<div class="card mb-3">
	<div class="card-body">
		<h5 class="card-title">Nahrát skladbu (MP3)</h5>
		{control uploadTrackForm:setDefaults(['station_id' => $station->id])}
	</div>
</div>

<div class="card mb-3">
	<div class="card-body">
		<h5 class="card-title">Tracky ve stanici</h5>

		{if $tracks->count('*') === 0}
			<p class="text-muted">Zatím žádné tracky.</p>
		{else}
			<table class="table table-sm">
				<thead>
					<tr>
						<th>Pozice</th>
						<th>Název</th>
						<th>Soubor</th>
						<th>Akce</th>
					</tr>
				</thead>
				<tbody>
					{foreach $tracks as $t}
						<tr>
							<td>{$t->position}</td>
							<td>{$t->title ?: $t->original_name}</td>
							<td>{$t->stored_name}</td>
							<td>
								<a n:href="deleteTrack!, id => $t->id"
									class="btn btn-sm btn-outline-danger"
									onclick="return confirm('Opravdu smazat skladbu?');">Smazat</a>
							</td>
						</tr>
					{/foreach}
				</tbody>
			</table>
		{/if}
	</div>
</div>

<div class="card">
	<div class="card-body">
		<h5 class="card-title">Ovládání služby</h5>

		<a n:href="restartStation!, slug => $station->slug"
			class="btn btn-warning">Restart stanice</a>

		<a n:href="deleteStation!, id => $station->id"
			class="btn btn-danger"
			onclick="return confirm('Opravdu smazat celou stanici včetně souborů?');">Smazat stanici</a>
	</div>
</div>
{/block}

Route do routeru

$router->addRoute('radio', 'Radio:default');
$router->addRoute('radio/create', 'Radio:create');
$router->addRoute('radio/edit/<id>', 'Radio:edit');
$router->addRoute('radio/show/<slug>', 'Radio:show');

services.neon (pokud je potřeba)

services:
	- App\UI\Radio\RadioPresenter

Diagnostika

OCOL diagnostika jedné instance:

sudo bash -lc '
set -e
LOG=/tmp/radio_web_03_diag_instance.log
exec > >(tee "$LOG") 2>&1

slug="ZDE_DOPLN_SLUG"

echo "== playlist =="
ls -l "/var/lib/radio/${slug}.m3u" || true
sed -n "1,120p" "/var/lib/radio/${slug}.m3u" || true

echo
echo "== systemd status =="
systemctl status "radio@${slug}.service" --no-pager || true

echo
echo "== journal tail =="
journalctl -u "radio@${slug}.service" -n 200 --no-pager || true

echo "DONE. LOG: $LOG"
'

Závěr

Máš webovou část rádioserveru: admin UI pro stanice, upload MP3 do stanice, generování M3U do /var/lib/radio a restart instance přes systemd. Serverová část zůstává beze změny – jen playlisty generuje Nette aplikace.


Komentáře: 0
Napište komentář

Rozhovor s Michalem o Betwindu, část 1

Rozhovor s Michalem Králem o kapele Betwind jako pokračování mojí kytarové cesty. V první části nahrávka písně Figure

jaký šel, takového potkal

Jak se dnešní přísloví odráží v našem životě a jak naše chování a postoje ovlivňují lidi, které potkáváme?

Rozhovor s Michalem o Betwindu, část 2

Rozhovor s Michalem Králem o kapele Betwind jako pokračování mojí kytarové cesty. Ve druhé části píseň Ophelia

Přání k Vánocům

Vánoční přání a nahrávka Chtíc, aby spal

Kráva zajíce nedohoní

Dnešní přísloví pojednává o realistických očekáváních a limitech schopností a o důležitosti přizpůsobení nároků reálným možnostem.

Kiksylend na Portě

Jaké to bylo na letošní portě? Působení Kiksylendu na legendárním hudebním festivalu, nahrávky písní Kavárenský povaleč, Kateřina a MDŽ a dále…

Dvakrát měř, jednou řež, aneb v podstatě, prostě, tedy a vlastně

Podívejme se na význam přísloví „Dvakrát měř, jednou řež“. Kdy se unáhlené kroky ukazují jako chyby? Proč je někdy lepší chvíli přemýšlet, než jednat?

Vzpomínky nejen školní, část 1: Gymnázium

Úsměvné vzpomínky na léta nejen školní. Přeji příjemný poslech

Vzpomínky nejen školní, část 4: Gymnázium naposledy

Závěrečný díl školních vzpomínek

Vzpomínky nejen školní, část 3: Gymnázium potřetí

Již třetí pokračování školních vzpomínek, tentokráte bude o panu řediteli a o gymnaziálním biologovi a chemikovi v jedné osobě. Přeji příjemný poslech

Foto z koncertů skupiny NAŠKRK, část I.

Galerie mých fotografií z koncertu skupiny NAŠKRK pořízené 15. 11. 2002. Foto: Roman Vokurek

Foto z koncertů skupiny NAŠKRK, část II.

Galerie mých fotografií z koncertu skupiny NAŠKRK v sále hotelu v Moravském Krumlově pořízené 7. března 2003. Foto: Roman Vokurek

Mariánská kaple v zimě

Fotky z lokality zvané jako Mariánská studánka mezi obcemi Moravský Krumlov a Rokytná, tentokráte pod sněhovou pokrývkou. Fotky pořízeny 10. 12. 2023…

K pramenům řeky Rokytné II.

Druhá výprava k pramenům posvátné řeky Rokytné tentokráte vedoucí z Moravského Krumlova do Tulešic. Průzkum proveden 19. 5. 2023. Autorem fotografií…

Výstup na Velký Lopeník

Druhá hřebenová výprava v nejkrásnějších horách světa, tedy v Bílých Karpatech, tentokráte zasvěcena dobytí Velkého Lopeníku (911 mnm). Výstup po…

Podzim v Moravském Krumlově, část II.

Výběr podzimních fotografií z Moravského Krumlova. Snímky pořízeny 12. 11. 2023. Foto: Petra

K pramenům řeky Rokytné III.

Třetí výprava k pramenům posvátné řeky Rokytné tentokráte vedoucí z Tulešic do Tavíkovic. Průzkum neprobádanou, neprostupnou, nedotčenou, divokou,…

Zima v Moravském Krumlově, část II.

Výběr zimních fotografií z Moravského Krumlova. Snímky pořízeny 2. 12. 2023. Foto: Petra

Jaro v Moravském Krumlově, část II.

Jarní galerie pořízená z moravskokrumlovského zámeckého parku 5. 5. 2024. Foto: Petra

K pramenům řeky Rokytné I.

Výprava k pramenům řeky Rokytné, část první od soutoku s Jihlavou v Ivančicích do Moravského Krumlova. Průzkum proveden 22. 4. 2022. Autorem…

Výlet do neznáma

Píseň z alba Rozpaky, inspirovaná neznámou dívkou v modrém... Text i hudba byli napsány 9. 12. 2018.

Podzim

Báseň a píseň z alba Rozpaky vydaná v roce 2019. Báseň byla napsána za letní noci 21. 8. 2016, nevzpomínám si přesně, ale myslím, že nad sklenkou whisky.

Třešně a led

Píseň z alba Rozpaky inspirovaná událostmi prvních lednových dní roku 2003. Píseň patří všem, kteří toho roku tragicky zahynuli... Text byl napsán 4. 7. 2016. Hudba byla napsána 27. 8. 2016.

Světem vcelku

Píseň z alba Rozpaky. Inspirace všeobímající, tak trochu osobní výpověď. Pojednání o věcech těžko pochopitelných. Text byl napsán 29. 11. 2015. Hudba byla napsána v lednu 2016.

Změna

Píseň z alba Rozpaky inspirovaná podzimními událostmi roku 2003. Text byl napsán 24. 8. 2016. Hudba byla napsána 20. 12. 2018.

Čumísek

Píseň z alba Rozpaky inspirovaná událostmi roku 2003. Text byl napsán 9. 7. 2016. Hudba byla napsána 17. 12. 2018.

Takový jako tento

Píseň z alba Rozpaky inspirovaná jak jinak nešťastnou láskou. Hudba původně z anglickým textem byla napsána v září 2003. Text byl napsán 20. 2. 2016.

Služby zdarma

Píseň z alba Rozpaky inspirovaná nehynoucí touhou všelijakých obchodníků prodat vám cokoli. Text i hudba byli napsány 10. 5. 2017.

Málem omylem

Vzpomínka na první červencové dny roku 2003. Pokud se vám náhodou něco někdy nepovede, nesahejte na alkohol, je to velice zlý pán a nemá slitování. Text písně byl napsán 8. 5. 2017.

Tak dávno

Vzpomínka na první červencové dny roku 2003. Text i hudba byli napsány 10. 5. 2017.

Video

Zbyněk Sklenský na vernisáži v Knížecím domě v Moravském Krumlově

Populární podcastové epizody

Všechny podcasty
Upovídaný podcast

Rozhovor s Michalem o Betwindu, část 1

Rozhovor s Michalem Králem o kapele Betwind jako pokračování mojí kytarové cesty. V první části nahrávka písně Figure

Zbyněk Sklenský

Zbyněk Sklenský

Přísloví pro každý den

jaký šel, takového potkal

Jak se dnešní přísloví odráží v našem životě a jak naše chování a postoje ovlivňují lidi, které potkáváme?

Rozhovor s Michalem o Betwindu, část 2

Rozhovor s Michalem Králem o kapele Betwind jako pokračování mojí kytarové cesty. Ve druhé části píseň Ophelia

Upovídaný podcast

Přání k Vánocům

Vánoční přání a nahrávka Chtíc, aby spal

Kráva zajíce nedohoní

Dnešní přísloví pojednává o realistických očekáváních a limitech schopností a o důležitosti přizpůsobení nároků reálným možnostem.

Kiksylend na Portě

Jaké to bylo na letošní portě? Působení Kiksylendu na legendárním hudebním festivalu, nahrávky písní Kavárenský povaleč, Kateřina a MDŽ a dále kratičké představení celkového vítěze Porty.

Dvakrát měř, jednou řež, aneb v podstatě, prostě, tedy a vlastně

Podívejme se na význam přísloví „Dvakrát měř, jednou řež“. Kdy se unáhlené kroky ukazují jako chyby? Proč je někdy lepší chvíli přemýšlet, než jednat?

Vzpomínky nejen školní, část 1: Gymnázium

Úsměvné vzpomínky na léta nejen školní. Přeji příjemný poslech

Vzpomínky nejen školní, část 4: Gymnázium naposledy

Závěrečný díl školních vzpomínek

Vzpomínky nejen školní, část 3: Gymnázium potřetí

Již třetí pokračování školních vzpomínek, tentokráte bude o panu řediteli a o gymnaziálním biologovi a chemikovi v jedné osobě. Přeji příjemný poslech

Podcast Fokální dystonie

Další epizody
Fokální dystonie

Fokální dystonie: O cestě k nápravě v roce 2022 obecně

Parafráze předchozí epizody, shrnutí metody Davida Leisnera a ukázky z jeho lekce. Seznámení s metodami Akiko Trush, Nory Krohn a Apostolose Paraskevase.

Zbyněk Sklenský

Zbyněk Sklenský

K pramenům řeky Rokytné I.

Fokální dystonie: O cestě k nápravě v první polovině roku 2022

Další pokračování seriálu o fokální dystonii. Metoda Davida Leisnera a jeho zapojení velkých svalů při hře na kytaru. Metoda Akiko Trush a její pozorování vlastních svalů při pohybu rukou. Dystonická aktivace a dystonická křeč.

Fokální dystonie: O cestě k nápravě ve 2. polovině roku 2021

Další pokračování cesty k nápravě, tzv. Period of Recovery, tentokráte popisující události druhé poloviny roku 2021. Jedna nahrávka z tohoto období a první kontakt s Davidem Leisnerem

K pramenům řeky Rokytné I.

Fokální dystonie: O cestě k nápravě v roce 2021

Další díl ze seriálu o fokální dystonii, tentokráte první díl o cestě k nápravě a o změně přístupu k této poruše, způsobené nevhodným dlouhodobým cvičením

Fokální dystonie: Non-artificiální období v letech 2019-2021 - Od pěti k nule

Další události vedoucí až k nemožnosti používat pravou ruku při jakýchkoli činnostech

Fokální dystonie: Non-artificiální období v letech 2018/2019

Další pokračování ze seriálu o fokální dystonii z doby, kdy porucha začala ovlivňovat celý můj život. Blessing in disguise - něco o angličtině, o programování a také poslední nahrávka z vystoupení s klasickou kytarou

Fokální dystonie, část 10: Non-artificiální období v letech 2015/18, Coming out

Další pokračování seriálu o fokální dystonii, tentokráte zahrnující období mezi lety 2015-2018

Fokální dystonie, část 9: Artificiální období v letech 2014-2015

Závěr studia na konzervatoři, vzpomínky, neklamné průvodní znaky fokální dystonie + nahrávky Cavatiny Stanley Meyerse a Garyoven Maura Giulianiho

Fokální dystonie, část 8: Artificiální období v letech 2012-2013

Završení maturitního ročníku, vzpomínky a první projevy fokální dystonie. Nahrávky Tarantelly J. K. Mertze a Simplicitas Jiřího Jirmala

Fokální dystonie, část 7: Artificiální období v letech 2012-2013

Pokračování povídání o mojí kytarové cestě, tentokráte popisující můj maturitní rok a další události s tím spojené. Jako vždy také i tentokrát připojuji své kytarové nahrávky.