diff --git a/appinfo/routes.php b/appinfo/routes.php
index 630fb091414e72bbb24168d8741e4a27cb8e8c38..63e76b9f904144e949cb68c1a37522bf8ebb3c71 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -23,7 +23,8 @@
 
 return [
     'resources' => [
-	'show' => ['url' => '/api/shows'],
+      'show' => ['url' => '/api/shows'],
+      'episode' => ['url' => '/api/episodes'],
   	],
     'routes' => [
 
diff --git a/lib/Controller/EpisodeController.php b/lib/Controller/EpisodeController.php
new file mode 100644
index 0000000000000000000000000000000000000000..64181bfb6ac1167e05fc8e27236790815a9ac9e0
--- /dev/null
+++ b/lib/Controller/EpisodeController.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * Podcast App
+ *
+ * @author Jonas Heinrich
+ * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Podcast\Controller;
+
+use OCA\Podcast\AppInfo\Application;
+use OCA\Podcast\Service\EpisodeService;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IRequest;
+
+class EpisodeController extends Controller {
+	/** @var EpisodesService */
+	private $service;
+
+	/** @var string */
+	private $userId;
+
+	use Errors;
+
+	public function __construct(IRequest $request,
+								EpisodeService $service,
+								$userId) {
+		parent::__construct(Application::APP_ID, $request);
+		$this->service = $service;
+		$this->userId = $userId;
+	}
+
+	/**
+	 * @NoAdminRequired
+	 */
+	public function index(): DataResponse {
+		return new DataResponse($this->service->findAll($this->userId));
+	}
+
+	/**
+	 * @NoAdminRequired
+	 */
+	public function show(int $id): DataResponse {
+		return $this->handleNotFound(function () use ($id) {
+			return $this->service->find($id, $this->userId);
+		});
+	}
+
+	/**
+	 * @NoAdminRequired
+	 */
+	 public function create(int $id, string $imgurl, string $author,
+	 	string $title, string $pubdate, string $duration, string $playtime,
+		string $lastplayed, string $enclosure): DataResponse {
+ 		return new DataResponse($this->service->create($id, $imgurl, $author,
+			$title, $pubdate, $duration, $playtime, $lastplayed, $enclosure,
+			$this->userId));
+ 	}
+
+	/**
+	 * @NoAdminRequired
+	 */
+	public function update(int $id, string $imgurl, string $author,
+		string $title, string $pubdate, string $duration, string $playtime,
+		string $lastplayed, string $enclosure): DataResponse {
+		return $this->handleNotFound(function () use ($id, $imgurl, $author,
+			$title, $pubdate, $duration, $playtime, $lastplayed, $enclosure) {
+			return $this->service->update($id, $imgurl, $author, $title, $pubdate,
+				$duration, $playtime, $lastplayed, $enclosure, $this->userId);
+ 		});
+ 	}
+
+	/**
+	 * @NoAdminRequired
+	 */
+	public function destroy(int $id): DataResponse {
+		return $this->handleNotFound(function () use ($id) {
+			return $this->service->delete($id, $this->userId);
+		});
+	}
+}
diff --git a/lib/Controller/ShowController.php b/lib/Controller/ShowController.php
index 086acb889811e574283e8e032d95ac058f9fadac..6e62bb1fb233e428e0144d5c216ab44f476684ed 100644
--- a/lib/Controller/ShowController.php
+++ b/lib/Controller/ShowController.php
@@ -74,9 +74,12 @@ class ShowController extends Controller {
 	/**
 	 * @NoAdminRequired
 	 */
-	public function update(int $id, string $title, string $htmlURL, string $smallImageURL, string $categories, string $lastpub, string $description, string $author): DataResponse {
-		return $this->handleNotFound(function () use ($id, $title, $htmlURL, $smallImageURL, $categories, $lastpub, $description, $author) {
-			return $this->service->update($id, $title, $htmlURL, $smallImageURL, $categories, $lastpub, $description, $author, $this->userId);
+	public function update(int $id, string $imgurl, string $author,
+	 	string $title): DataResponse {
+		return $this->handleNotFound(function () use ($id, $imgurl, $author,
+			$title) {
+			return $this->service->update($id, $imgurl, $author, $title,
+				$this->userId);
  		});
  	}
 
diff --git a/lib/Db/Episode.php b/lib/Db/Episode.php
new file mode 100644
index 0000000000000000000000000000000000000000..470478f7fb9743e3b94bbef5b49902b3b6acb176
--- /dev/null
+++ b/lib/Db/Episode.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Podcast App
+ *
+ * @author Jonas Heinrich
+ * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Podcast\Db;
+
+use JsonSerializable;
+
+use OCP\AppFramework\Db\Entity;
+
+class Episode extends Entity implements JsonSerializable {
+	protected $imgurl;
+	protected $author;
+	protected $title;
+	protected $pubdate;
+	protected $duration;
+	protected $playtime;
+	protected $lastplayed;
+	protected $enclosure;
+	protected $userId;
+
+	public function jsonSerialize(): array {
+		return [
+			'id' => $this->id,
+			'imgurl' => $this->imgurl,
+			'author' => $this->author,
+			'title' => $this->title,
+			'pubdate' => $this->pubdate,
+			'duration' => $this->duration,
+			'playtime' => $this->playtime,
+			'lastplayed' => $this->lastplayed,
+			'enclosure' => $this->enclosure,
+		];
+	}
+}
diff --git a/lib/Db/EpisodeMapper.php b/lib/Db/EpisodeMapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..654242d59e57437ff7e33111c93afa58d8c40997
--- /dev/null
+++ b/lib/Db/EpisodeMapper.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * Podcast App
+ *
+ * @author Jonas Heinrich
+ * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Podcast\Db;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\Entity;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+class EpisodeMapper extends QBMapper {
+	public function __construct(IDBConnection $db) {
+		parent::__construct($db, 'episodes', Episode::class);
+	}
+
+	/**
+	 * @param int $id
+	 * @param string $userId
+	 * @return Entity|Episode
+	 * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
+	 * @throws DoesNotExistException
+	 */
+	public function find(int $id, string $userId): Episode {
+		/* @var $qb IQueryBuilder */
+		$qb = $this->db->getQueryBuilder();
+		$qb->select('*')
+			->from('episodes')
+			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
+			->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
+		return $this->findEntity($qb);
+	}
+
+	/**
+	 * @param string $userId
+	 * @return array
+	 */
+	public function findAll(string $userId): array {
+		/* @var $qb IQueryBuilder */
+		$qb = $this->db->getQueryBuilder();
+		$qb->select('*')
+			->from('episodes')
+			->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
+		return $this->findEntities($qb);
+	}
+}
diff --git a/lib/Migration/Version000000Date20181013124731.php b/lib/Migration/Version000000Date20181013124731.php
index a0b60ccee3599b6a605764788684fad760416684..234122265ac5b165bda118e3fcbff485d84ef02d 100644
--- a/lib/Migration/Version000000Date20181013124731.php
+++ b/lib/Migration/Version000000Date20181013124731.php
@@ -57,6 +57,26 @@ class Version000000Date20181013124731 extends SimpleMigrationStep {
 			$table->addIndex(['user_id'], 'shows_user_id_index');
 		}
 
+		if (!$schema->hasTable('episodes')) {
+			$table = $schema->createTable('episodes');
+			$table->addColumn('id', 'integer', [
+				'notnull' => true,
+			]);
+      $table->addColumn('user_id', 'string', [
+				'notnull' => true,
+			]);
+			$table->addColumn('imgurl', 'string');
+			$table->addColumn('author', 'string');
+			$table->addColumn('title', 'string');
+			$table->addColumn('pubdate', 'string');
+			$table->addColumn('duration', 'string');
+			$table->addColumn('playtime', 'string');
+			$table->addColumn('lastplayed', 'string');
+			$table->addColumn('enclosure', 'string');
+			$table->setPrimaryKey(['id']);
+			$table->addIndex(['user_id'], 'shows_user_id_index');
+		}
+
 		return $schema;
 	}
 }
diff --git a/lib/Service/EpisodeService.php b/lib/Service/EpisodeService.php
new file mode 100644
index 0000000000000000000000000000000000000000..f5bb1509000aff570b11d9f32dd245797e4a7133
--- /dev/null
+++ b/lib/Service/EpisodeService.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * Podcast App
+ *
+ * @author Jonas Heinrich
+ * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Podcast\Service;
+
+use Exception;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+
+use OCA\Podcast\Db\Episode;
+use OCA\Podcast\Db\EpisodeMapper;
+
+class EpisodeService {
+
+	/** @var EpisodeMapper */
+	private $mapper;
+
+	public function __construct(EpisodeMapper $mapper) {
+		$this->mapper = $mapper;
+	}
+
+	public function findAll(string $userId): array {
+		return $this->mapper->findAll($userId);
+	}
+
+	private function handleException(Exception $e): void {
+		if ($e instanceof DoesNotExistException ||
+			$e instanceof MultipleObjectsReturnedException) {
+			throw new EpisodeNotFound($e->getMessage());
+		} else {
+			throw $e;
+		}
+	}
+
+	public function find($id, $userId) {
+		try {
+			return $this->mapper->find($id, $userId);
+
+			// in order to be able to plug in different storage backends like files
+		// for instance it is a good idea to turn storage related exceptions
+		// into service related exceptions so controllers and service users
+		// have to deal with only one type of exception
+		} catch (Exception $e) {
+			$this->handleException($e);
+		}
+	}
+
+	public function create($id, $imgurl, $author, $title, $pubdate, $duration,
+		$playtime, $lastplayed, $enclosure, $userId) {
+		$episode = new Episode();
+		$episode->setId($id);
+		$episode->setImgurl($imgurl);
+		$episode->setAuthor($author);
+		$episode->setTitle($title);
+		$episode->setPubdate($pubdate);
+		$episode->setDuration($duration);
+		$episode->setPlaytime($playtime);
+		$episode->setLastplayed($lastplayed);
+		$episode->setEnclosure($enclosure);
+		$episode->setUserId($userId);
+		return $this->mapper->insert($episode);
+	}
+
+	public function update($id, $imgurl, $author, $title, $pubdate, $duration,
+	 	$playtime, $lastplayed, $enclosure, $userId) {
+		try {
+			$episode = $this->mapper->find($id, $userId);
+			$episode->setImgurl($imgurl);
+			$episode->setAuthor($author);
+			$episode->setTitle($title);
+			$episode->setPubdate($pubdate);
+			$episode->setDuration($duration);
+			$episode->setPlaytime($playtime);
+			$episode->setLastplayed($lastplayed);
+			$episode->setEnclosure($enclosure);
+			return $this->mapper->update($episode);
+		} catch (Exception $e) {
+			$this->handleException($e);
+		}
+	}
+
+	public function delete($id, $userId) {
+		try {
+			$episode = $this->mapper->find($id, $userId);
+			$this->mapper->delete($episode);
+			return $episode;
+		} catch (Exception $e) {
+			$this->handleException($e);
+		}
+	}
+}
diff --git a/lib/Service/ShowService.php b/lib/Service/ShowService.php
index 839a106ac385f91b34ca7f428750c779a6a5b0ce..d714c8889f3d44cd6b4d440232bdeefaf3e98b50 100644
--- a/lib/Service/ShowService.php
+++ b/lib/Service/ShowService.php
@@ -76,16 +76,12 @@ class ShowService {
 		return $this->mapper->insert($show);
 	}
 
-	public function update($id, $title, $htmlURL, $smallImageURL, $categories, $lastpub, $description, $author, $userId) {
+	public function update($id, $imgurl, $author, $title, $userId) {
 		try {
 			$show = $this->mapper->find($id, $userId);
-			$show->setTitle($title);
-			$show->setHtmlURL($htmlURL);
-			$show->setSmallImageURL($smallImageURL);
-			$show->setCategories($categories);
-			$show->setLastpub($lastpub);
-			$show->setDescription($description);
+			$show->setImgurl($imgurl);
 			$show->setAuthor($author);
+			$show->setTitle($title);
 			return $this->mapper->update($show);
 		} catch (Exception $e) {
 			$this->handleException($e);
diff --git a/src/App.vue b/src/App.vue
index 2ab917cf37ab94f38fd065876f1a029a6c35c498..ccb1c4f4447847255d2e7a381ed25ab8732ea2c9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -43,6 +43,7 @@ export default {
 	},
 	created() {
 		this.$store.dispatch('loadShows')
+		this.$store.dispatch('loadEpisodes')
 		this.$store.dispatch('getVolumeState')
 	},
 }
diff --git a/src/components/MediaHeader.vue b/src/components/MediaHeader.vue
index 9047894c2aaf272c9b32523b6315c6c7f6ce9f04..db48e5c454cdf65297f0551355ffd16db191c506 100644
--- a/src/components/MediaHeader.vue
+++ b/src/components/MediaHeader.vue
@@ -59,7 +59,9 @@
 
 <script>
 import vueShowMoreText from 'vue-show-more-text'
-import { getCategoryName } from '../services/podcastApi'
+import { ShowApi } from './../services/ShowApi'
+
+const apiClient = new ShowApi()
 
 export default {
 	name: 'MediaHeader',
@@ -119,7 +121,7 @@ export default {
 			this.$emit('doSubscribe')
 		},
 		getCategoryName(categoryid) {
-			return getCategoryName(categoryid)
+			return apiClient.getCategoryName(categoryid)
 		},
 	},
 }
diff --git a/src/services/EpisodeApi.js b/src/services/EpisodeApi.js
new file mode 100644
index 0000000000000000000000000000000000000000..6303885e6e8d3bd7e50496a40c6f09d2718f9da3
--- /dev/null
+++ b/src/services/EpisodeApi.js
@@ -0,0 +1,89 @@
+/*
+ * @copyright Copyright (c) 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @author Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+const requesttoken = axios.defaults.headers.requesttoken
+
+export class EpisodeApi {
+
+	url(url) {
+		url = `/apps/podcast/api${url}`
+		return generateUrl(url)
+	}
+
+	addEpisode(episode) {
+		episode = {
+			id: episode.id,
+			imgurl: episode.smallImageURL,
+			title: episode.title,
+			author: episode.author,
+		}
+		axios.defaults.headers.requesttoken = requesttoken
+		return axios.post(this.url('/episodes'), episode)
+			.then(
+				(response) => {
+					return Promise.resolve(response.data)
+				},
+				(err) => {
+					return Promise.reject(err)
+				}
+			)
+			.catch((err) => {
+				return Promise.reject(err)
+			})
+	}
+
+	removeEpisode(episode) {
+		axios.defaults.headers.requesttoken = requesttoken
+		return axios.delete(this.url(`/episodes/${episode.id}`))
+			.then(
+				(response) => {
+					return Promise.resolve(response.data)
+				},
+				(err) => {
+					return Promise.reject(err)
+				}
+			)
+			.catch((err) => {
+				return Promise.reject(err)
+			})
+	}
+
+	loadEpisodes(episode) {
+		axios.defaults.headers.requesttoken = requesttoken
+		return axios.get(this.url('/episodes'))
+			.then(
+				(response) => {
+					return Promise.resolve(response.data)
+				},
+				(err) => {
+					return Promise.reject(err)
+				}
+			)
+			.catch((err) => {
+				return Promise.reject(err)
+			})
+	}
+
+}
diff --git a/src/services/player.js b/src/services/Player.js
similarity index 53%
rename from src/services/player.js
rename to src/services/Player.js
index 55dce688f88d04353f332f21aa7009f7ba1830da..93ea8a254158890658db7aa56b7111f22219702e 100644
--- a/src/services/player.js
+++ b/src/services/Player.js
@@ -23,39 +23,38 @@
 import { Howl, Howler } from 'howler'
 import { showError } from '@nextcloud/dialogs'
 
-export let audioPlayer
-audioPlayer = null
+let audioPlayer = null
 
-export function doPlay(episode) {
-	const vm = this
+export class Player {
 
-	vm.$store.dispatch('isBuffering', true)
-
-	if (audioPlayer !== null) {
-		audioPlayer.fade(vm.player.volume, 0, 500)
+	play(src) {
+		this.$store.dispatch('isBuffering', true)
+		if (audioPlayer !== null) {
+			audioPlayer.fade(this.player.volume, 0, 500)
+		}
+		this.$store.dispatch('setTitle', 'test')
+		Howler.unload()
+		audioPlayer = new Howl({
+			src,
+			html5: true,
+			volume: this.player.volume,
+			onplay() {
+				this.$store.dispatch('isPlaying', true)
+				this.$store.dispatch('isBuffering', false)
+			},
+			onpause() {
+				this.$store.dispatch('isPlaying', false)
+				this.$store.dispatch('isBuffering', false)
+			},
+			onend() {
+				showError(t('podcast', 'Lost connection to podcast station, retrying ...'))
+				this.$store.dispatch('isPlaying', false)
+				this.$store.dispatch('isBuffering', true)
+			},
+		})
+		audioPlayer.unload()
+		audioPlayer.play()
+		audioPlayer.fade(0, this.player.volume, 500)
 	}
-	vm.$store.dispatch('setTitle', episode.title)
 
-	Howler.unload()
-	audioPlayer = new Howl({
-		src: episode.enclosure,
-		html5: true,
-		volume: vm.player.volume,
-		onplay() {
-			vm.$store.dispatch('isPlaying', true)
-			vm.$store.dispatch('isBuffering', false)
-		},
-		onpause() {
-			vm.$store.dispatch('isPlaying', false)
-			vm.$store.dispatch('isBuffering', false)
-		},
-		onend() {
-			showError(t('podcast', 'Lost connection to podcast station, retrying ...'))
-			vm.$store.dispatch('isPlaying', false)
-			vm.$store.dispatch('isBuffering', true)
-		},
-	})
-	audioPlayer.unload()
-	audioPlayer.play()
-	audioPlayer.fade(0, vm.player.volume, 500)
 }
diff --git a/src/services/ShowApi.js b/src/services/ShowApi.js
index 070508c4718b94cfadaf77f3de249328e338bd43..f479815a69bd795ffe43f17596fa593ea45e1b3f 100644
--- a/src/services/ShowApi.js
+++ b/src/services/ShowApi.js
@@ -24,6 +24,7 @@ import axios from '@nextcloud/axios'
 import { generateUrl } from '@nextcloud/router'
 
 const requesttoken = axios.defaults.headers.requesttoken
+const categories = require('../assets/categories.json')
 
 export class ShowApi {
 
@@ -86,4 +87,23 @@ export class ShowApi {
 			})
 	}
 
+	getCategoryName(categoryid) {
+		for (let i = 0; i < categories.data.length; i++) {
+			const obj = categories.data[i]
+			if (obj.id === categoryid) {
+				return obj.name
+			}
+			if ('subcategories' in categories.data[i]) {
+				const subcategories = categories.data[i].subcategories
+				for (let i = 0; i < subcategories.length; i++) {
+					const obj = subcategories[i]
+					if (obj.id === categoryid) {
+						return obj.name
+					}
+				}
+			}
+		}
+		return categoryid
+	}
+
 }
diff --git a/src/services/podcastApi.js b/src/services/podcastApi.js
deleted file mode 100644
index b7748f81cfe8319929cf69df9c60a40612388523..0000000000000000000000000000000000000000
--- a/src/services/podcastApi.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * @copyright Copyright (c) 2021 Jonas Heinrich <onny@project-insanity.org>
- *
- * @author Jonas Heinrich <onny@project-insanity.org>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-const categories = require('../assets/categories.json')
-
-export function getCategoryName(categoryid) {
-	for (let i = 0; i < categories.data.length; i++) {
-		const obj = categories.data[i]
-		if (obj.id === categoryid) {
-			return obj.name
-		}
-		if ('subcategories' in categories.data[i]) {
-			const subcategories = categories.data[i].subcategories
-			for (let i = 0; i < subcategories.length; i++) {
-				const obj = subcategories[i]
-				if (obj.id === categoryid) {
-					return obj.name
-				}
-			}
-		}
-	}
-	return categoryid
-}
diff --git a/src/store/episode.js b/src/store/episode.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b0899c097f8c2412f66bc2f659f099e9096733d
--- /dev/null
+++ b/src/store/episode.js
@@ -0,0 +1,75 @@
+/*
+ * @copyright Copyright (c) 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @author Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import { EpisodeApi } from './../services/EpisodeApi'
+
+const apiClient = new EpisodeApi()
+
+export default {
+	state: {
+		episodes: [],
+	},
+	getters: {
+		episodesQueue: state => {
+			return state.episodes
+		},
+		episodeById: state => (id) => {
+			return state.episodes.find((episode) => episode.id === id)
+		},
+		episodeExists: state => (id) => {
+			return state.episodes.some((episode) => episode.id === id)
+		},
+
+	},
+	mutations: {
+		addEpisode(state, episode) {
+			state.episodes.push(episode)
+		},
+		removeEpisode(state, episode) {
+			const existingIndex = state.episodes.findIndex(_episode => _episode.id === episode.id)
+			if (existingIndex !== -1) {
+				state.episodes.splice(existingIndex, 1)
+			}
+		},
+		setEpisodes(state, episodes) {
+			state.episodes = episodes
+		},
+	},
+	actions: {
+		async loadEpisodes({ commit }) {
+			const episodes = await apiClient.loadEpisodes()
+			commit('setEpisodes', episodes)
+		},
+		addEpisode({ commit }, episode) {
+			apiClient.addEpisode(episode)
+				.then((episode) => {
+					commit('addEpisode', episode)
+				})
+		},
+		removeEpisode({ commit }, episode) {
+			apiClient.removeEpisode(episode)
+				.then((episode) => {
+					commit('removeEpisode', episode)
+				})
+		},
+	},
+}
diff --git a/src/store/main.js b/src/store/main.js
index 8aa08e653cbdb72bb54a8601794aab23cd2f50b3..d3260ea7731bff1064096e9363492ed0504e4e9d 100644
--- a/src/store/main.js
+++ b/src/store/main.js
@@ -23,106 +23,16 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 
-import axios from '@nextcloud/axios'
-import { generateUrl } from '@nextcloud/router'
-
 import show from './show'
 import player from './player'
+import episode from './episode'
 
 Vue.use(Vuex)
-const requesttoken = axios.defaults.headers.requesttoken
 
 export default new Vuex.Store({
 	modules: {
 		show,
 		player,
-	},
-	state: {
-		player: {
-			isPlaying: false,
-			isBuffering: false,
-			isMute: false,
-			isPaused: false,
-			volume: 0.5,
-			oldVolume: 0,
-			title: '',
-		},
-		menu: 'top',
-	},
-	mutations: {
-		isPlaying(state, playerState) {
-			state.player.isPlaying = playerState
-		},
-		isBuffering(state, bufferingState) {
-			state.player.isBuffering = bufferingState
-		},
-		changeVolume(state, volume) {
-			state.player.volume = volume
-		},
-		toggleMute(state) {
-			if (state.player.isMute) {
-				state.player.volume = state.player.oldVolume
-				state.player.isMute = false
-			} else {
-				state.player.oldVolume = state.player.volume
-				state.player.volume = 0
-				state.player.isMute = true
-			}
-		},
-		togglePlay(state) {
-			if (state.player.isPlaying) {
-				state.player.isPlaying = false
-				state.player.isPaused = true
-			} else {
-				state.player.isPlaying = true
-				state.player.isPaused = false
-			}
-		},
-		setTitle(state, title) {
-			state.player.title = title
-		},
-		setVolumeState(state, volumeState) {
-			axios.defaults.headers.requesttoken = requesttoken
-			axios.post(generateUrl('/apps/podcast/settings/volumeState'), {
-				volumeState,
-			})
-		},
-		getVolumeState(state) {
-			axios.defaults.headers.requesttoken = requesttoken
-			axios
-				.get(generateUrl('/apps/podcast/settings/volumeState'))
-				.then(async response => {
-					const {
-						data: { volumeState: value },
-					} = response
-					state.player.volume = value
-				})
-		},
-	},
-	actions: {
-		isPlaying(context, playerState) {
-			context.commit('isPlaying', playerState)
-		},
-		isBuffering(context, bufferingState) {
-			context.commit('isBuffering', bufferingState)
-		},
-		changeVolume(context, volume) {
-			context.commit('changeVolume', volume)
-		},
-		toggleMute(context) {
-			context.commit('toggleMute')
-		},
-		togglePlay(context) {
-			context.commit('togglePlay')
-		},
-		setTitle(context, title) {
-			context.commit('setTitle', title)
-		},
-		setVolumeState(context, volumeState) {
-			context.commit('setVolumeState', volumeState)
-		},
-		getVolumeState(context) {
-			context.commit('getVolumeState')
-		},
+		episode,
 	},
 })
diff --git a/src/store/player.js b/src/store/player.js
new file mode 100644
index 0000000000000000000000000000000000000000..45944b0b0df1fa87bdc69ba9cc164bdbc6dc57a8
--- /dev/null
+++ b/src/store/player.js
@@ -0,0 +1,114 @@
+/*
+ * @copyright Copyright (c) 2021 Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @author Jonas Heinrich <onny@project-insanity.org>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+const requesttoken = axios.defaults.headers.requesttoken
+
+export default {
+	state: {
+		isPlaying: false,
+		isBuffering: false,
+		isMute: false,
+		isPaused: false,
+		volume: 0.5,
+		oldVolume: 0,
+		title: '',
+		episodePlaying: null,
+	},
+	mutations: {
+		isPlaying(state, playerState) {
+			state.player.isPlaying = playerState
+		},
+		isBuffering(state, bufferingState) {
+			state.player.isBuffering = bufferingState
+		},
+		changeVolume(state, volume) {
+			state.player.volume = volume
+		},
+		toggleMute(state) {
+			if (state.player.isMute) {
+				state.player.volume = state.player.oldVolume
+				state.player.isMute = false
+			} else {
+				state.player.oldVolume = state.player.volume
+				state.player.volume = 0
+				state.player.isMute = true
+			}
+		},
+		togglePlay(state) {
+			if (state.player.isPlaying) {
+				state.player.isPlaying = false
+				state.player.isPaused = true
+			} else {
+				state.player.isPlaying = true
+				state.player.isPaused = false
+			}
+		},
+		setTitle(state, title) {
+			state.player.title = title
+		},
+		setVolumeState(state, volumeState) {
+			axios.defaults.headers.requesttoken = requesttoken
+			axios.post(generateUrl('/apps/podcast/settings/volumeState'), {
+				volumeState,
+			})
+		},
+		getVolumeState(state) {
+			axios.defaults.headers.requesttoken = requesttoken
+			axios
+				.get(generateUrl('/apps/podcast/settings/volumeState'))
+				.then(async response => {
+					const {
+						data: { volumeState: value },
+					} = response
+					state.player.volume = value
+				})
+		},
+	},
+	actions: {
+		isPlaying(context, playerState) {
+			context.commit('isPlaying', playerState)
+		},
+		isBuffering(context, bufferingState) {
+			context.commit('isBuffering', bufferingState)
+		},
+		changeVolume(context, volume) {
+			context.commit('changeVolume', volume)
+		},
+		toggleMute(context) {
+			context.commit('toggleMute')
+		},
+		togglePlay(context) {
+			context.commit('togglePlay')
+		},
+		setTitle(context, title) {
+			context.commit('setTitle', title)
+		},
+		setVolumeState(context, volumeState) {
+			context.commit('setVolumeState', volumeState)
+		},
+		getVolumeState(context) {
+			context.commit('getVolumeState')
+		},
+	},
+}
diff --git a/src/views/Episode.vue b/src/views/Episode.vue
index 13e0a965cd9fedf9a6dc60f93587b37cdd2e171f..61f04677e3f54428784a441f7ccaa318f39bf69e 100644
--- a/src/views/Episode.vue
+++ b/src/views/Episode.vue
@@ -30,7 +30,9 @@
 					:author="podcastName"
 					:htmlurl="`#/browse/show/${podcastId}`"
 					:isshow="false">
-					<button class="icon-play-white podcastButton button primary new-button">
+					<button
+						class="icon-play-white podcastButton button primary new-button"
+						@click="doPlay">
 						{{ t('podcast', 'Play episode') }}
 					</button>
 					<div class="episodeDetails">
@@ -84,7 +86,10 @@ import MediaHeader from '../components/MediaHeader'
 import TimeAgo from 'javascript-time-ago'
 
 import { FyydApi } from './../services/FyydApi'
+import { Player } from './../services/Player'
+
 const fyydClient = new FyydApi()
+const player = new Player()
 
 const timeAgo = new TimeAgo('en-US')
 
@@ -110,6 +115,10 @@ export default {
 	},
 	methods: {
 
+		doPlay() {
+			player.play(this.episode.enclosure)
+		},
+
 		readableDuration(timestamp) {
 			return timestamp.split('.')[0]
 		},
diff --git a/src/views/Show.vue b/src/views/Show.vue
index f0f0d6bb2466d0a02d367927f311415029433895..112e27e0839deb348ea18f53d66d4032ddfd45ab 100644
--- a/src/views/Show.vue
+++ b/src/views/Show.vue
@@ -52,9 +52,9 @@ import Table from '../components/Table'
 import MediaHeader from '../components/MediaHeader'
 import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
 import ShowEmpty from './placeholder/Show'
-import { audioPlayer, doPlay } from '../services/player'
 
 import { FyydApi } from './../services/FyydApi'
+
 const fyydClient = new FyydApi()
 
 export default {
@@ -85,18 +85,6 @@ export default {
 			this.podcastId = to.params.id
 			this.init()
 		},
-		'player.volume'(newVolume, oldVolume) {
-			if (audioPlayer !== null) {
-				audioPlayer.volume(newVolume)
-			}
-		},
-		'player.isPaused'(newState, oldState) {
-			if (newState === true && audioPlayer !== null) {
-				audioPlayer.pause()
-			} else if (newState === false && audioPlayer !== null) {
-				audioPlayer.play()
-			}
-		},
 	},
 	mounted() {
 		this.initPage()
@@ -129,10 +117,6 @@ export default {
 			}
 		},
 
-		doPlay(episode) {
-			doPlay(episode)
-		},
-
 		doSubscribe() {
 			if (this.isSubscribed) {
 				this.$store.dispatch('removeShow', this.podcast)