diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8a654dbe0af75bb8f9d9e61f33af9830723746e2..ad17d63f9cbcf5cf00490bbb529d446ee2cbe648 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -8,6 +8,7 @@ tester:
script:
- apt-get update -qy
- apt-get install -y python3-dev python3-pip python3-pyflakes python3-pytest
+ - pip3 install -r requirements.txt
- PYTHONPATH=python pyflakes3 python/podcast/*py
- PYTHONPATH=python pyflakes3 qml/components/*py
- PYTHONPATH=./python python3 -m pytest test
diff --git a/README.md b/README.md
index dc7b31e12fee5de0857af4685d458f30a08507d8..7be98320b7f2f2aefb17a47dc84d8d49a49c9032 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
# PodQast
A queue based podcast player for SailfishOS
+# Testing
+Tests can be run with pytest from the test directory.
+
## Author
Thomas Renard [podqast@g3la.de](mailto:podqast@g3la.de)
diff --git a/harbour-podqast.pro b/harbour-podqast.pro
index fad40acb521d4d791c821c831f4dda26b14d6669..4d5a26b52008f40c2d22ba7dbf485904c3596e9a 100644
--- a/harbour-podqast.pro
+++ b/harbour-podqast.pro
@@ -17,7 +17,9 @@ CONFIG += sailfishapp_qml
DISTFILES += qml/podqast.qml \
qml/components/FyydDe.py \
qml/components/FyydDePython.qml \
+ qml/components/MigrationHandler.qml \
qml/cover/CoverPage.qml \
+ qml/pages/DataMigration.qml \
qml/pages/DiscoverFyyd.qml \
rpm/podqast.changes.in \
rpm/podqast.changes.run.in \
@@ -44,7 +46,7 @@ DISTFILES += qml/podqast.qml \
python/mygpoclient/simple_test.py \
python/mygpoclient/testing.py \
python/mygpoclient/util.py \
- python/feedparser.py \
+ python/feedparser/ \
qml/params.yml \
qml/cover/CoverPage.qml \
qml/pages/GpodderNetPython.qml \
@@ -90,7 +92,8 @@ DISTFILES += qml/podqast.qml \
qml/pages/FilePicker.qml \
qml/pages/DiscoverExport.qml \
qml/pages/External.qml \
- qml/components/ExternalHandlerPython.qml
+ qml/components/ExternalHandlerPython.qml \
+ qml/components/BackupButtons.qml
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172
diff --git a/python/podcast/__init__.py b/python/podcast/__init__.py
index 5d07396771dbdab99c8578ebd286f6dba14bf387..0c5c949f032d6ca539bf9060f78e20d69641cfac 100644
--- a/python/podcast/__init__.py
+++ b/python/podcast/__init__.py
@@ -1,3 +1,5 @@
"""
podcast classes
"""
+import logging
+logging.basicConfig(level=logging.INFO)
diff --git a/python/podcast/data_migration.py b/python/podcast/data_migration.py
new file mode 100644
index 0000000000000000000000000000000000000000..b278fa04cbbc93b08d517faf4ba8a7c6e51d6713
--- /dev/null
+++ b/python/podcast/data_migration.py
@@ -0,0 +1,104 @@
+import logging
+import os
+import pyotherside
+
+from podcast.factory import Factory
+from podcast.podcast import PodcastFactory, Podcast
+from podcast.podcastlist import PodcastListFactory
+from podcast.podpost import Podpost
+
+logger = logging.getLogger(__name__)
+
+
+def needs_migration():
+ target_data_version = 1
+ def app_is_pristine():
+ return len(list(PodcastListFactory().get_podcast_list().get_podcasts())) == 0
+ if get_versionnumber() >= target_data_version:
+ return False
+ else:
+ if app_is_pristine():
+ set_versionnumber(target_data_version)
+ return False
+ else:
+ return True
+
+def run_migrations():
+ current_version = get_versionnumber()
+ if(current_version < 1):
+ logger.info("Migrating from version %d to version 1",current_version)
+ podcasts = list(PodcastListFactory().get_podcast_list().get_podcasts())
+ pyotherside.send("migrationStart", len(podcasts))
+ i : int = 0
+ for podcast_url in podcasts:
+ podcast = PodcastFactory().get_podcast(podcast_url)
+ if podcast != None:
+ migrate_podcast_v0_v1(podcast)
+ for entry_id in podcast.entry_ids_old_to_new:
+ migrate_podpost_v0_v1(Factory().get_podpost(entry_id))
+ i += 1
+ pyotherside.send("migrationProgress", i)
+ set_versionnumber(1)
+ pyotherside.send("migrationDone")
+
+
+
+def get_versionnumber() -> int:
+ versionfilepath = get_versionfile_path()
+ if os.path.isfile(versionfilepath):
+ with open(versionfilepath, 'r') as versionfile:
+ return int(versionfile.read())
+ else:
+ return 0
+
+
+def set_versionnumber(number: int):
+ versionfilepath = get_versionfile_path()
+ with open(versionfilepath, 'w') as versionfile:
+ versionfile.write(str(number))
+
+
+def get_versionfile_path():
+ return os.path.join(Factory().data_home, "dataversion")
+
+def migrate_podpost_v0_v1(self: Podpost):
+ self.isaudio = False
+ self.save()
+
+def migrate_podcast_v0_v1(self: Podcast):
+ logger.info("migrating podcast '%s' to new format version 1", self.title)
+ try:
+ from podcast.factory import Factory
+ if self.logo_path:
+ image = self.logo_path
+ else:
+ image = self.logo_url
+ entry_ids_new_to_old = []
+ old_entry_count = 0
+ if self.feed is not None:
+ old_entry_count = len(self.feed.entries)
+ logger.info("Found %d entries in old format", old_entry_count)
+ for entry in self.feed.entries:
+ post = Podpost(entry, self.url, image)
+ p2 = Factory().get_podpost(post.id)
+ if p2:
+ post = p2
+ post.save()
+ entry_ids_new_to_old.append(post.id)
+ self._set_alt_feeds(self.feed)
+ else:
+ logger.warning("feed for %s was None. Will still continue migration but leave entries empty.",
+ self.title)
+ entry_ids_new_to_old.reverse()
+ self.entry_ids_old_to_new = entry_ids_new_to_old
+ logger.info("Found %d entries in new format", len(self.entry_ids_old_to_new))
+ self.feed = None
+ if old_entry_count == len(entry_ids_new_to_old):
+ logger.info("migration done")
+ self.save()
+ else:
+ logger.error("Could not complete migration. old count: %d, new: %d", old_entry_count,
+ len(entry_ids_new_to_old))
+ except:
+ logger.exception("error while migrating podcast %s to new format", self.title)
+
diff --git a/python/podcast/factory.py b/python/podcast/factory.py
index 01a4109f47222f3f517d80620cf5e74ec1d6c4a9..15d7dca4e3f04cff5b5e2083035fb8cafa562878 100644
--- a/python/podcast/factory.py
+++ b/python/podcast/factory.py
@@ -24,6 +24,7 @@ class Factory(metaclass=Singleton):
"""
Factory for
"""
+ data_home: str
def __init__(self, progname="harbour-podqast"):
"""
@@ -36,19 +37,7 @@ class Factory(metaclass=Singleton):
self.feedcache = Cache()
self.object = None
- home = os.path.expanduser("~")
- xdg_data_home = os.environ.get(
- "XDG_DATA_HOME", os.path.join(home, ".local", "share")
- )
- xdg_config_home = os.environ.get(
- "XDG_CONFIG_HOME", os.path.join(home, ".config")
- )
- xdg_cache_home = os.path.join(
- "XDG_CACHE_HOME", os.path.join(home, ".cache")
- )
- self.data_home = os.path.join(xdg_data_home, progname)
- self.config_home = os.path.join(xdg_config_home, progname)
- self.cache_home = os.path.join(xdg_cache_home, progname)
+ self._set_paths(progname)
if "PODQAST_HOME" in os.environ:
home = os.environ["PODQAST_HOME"]
@@ -66,7 +55,23 @@ class Factory(metaclass=Singleton):
self.store = Store(storepath)
self.podpostcache = Cache(limit=500)
- def get_object(self, object):
+ def _set_paths(self, progname):
+ home = os.path.expanduser("~")
+ xdg_data_home = os.environ.get(
+ "XDG_DATA_HOME", os.path.join(home, ".local", "share")
+ )
+ xdg_config_home = os.environ.get(
+ "XDG_CONFIG_HOME", os.path.join(home, ".config")
+ )
+ xdg_cache_home = os.environ.get(
+ "XDG_CACHE_HOME", os.path.join(home, ".cache")
+ )
+ self.data_home = os.path.join(xdg_data_home, progname)
+ self.config_home = os.path.join(xdg_config_home, progname)
+ self.cache_home = os.path.join(xdg_cache_home, progname)
+
+
+ def init_from_qml(self, object):
"""
Get QML object
"""
@@ -78,12 +83,11 @@ class Factory(metaclass=Singleton):
pass
self.object = object
-
home = os.path.expanduser("~")
-
self.media_home = os.path.join(self.get_val("musicHomeVal"), "podqast")
self.favorites_home = os.path.join(self.media_home, "favorites")
self.external_home = os.path.join(self.media_home, "external")
+
xdg_media_home = os.path.join(
"XDG_DATA_DIRS", os.path.join(home, "podqast")
)
@@ -158,6 +162,10 @@ class Factory(metaclass=Singleton):
return podpost
+ def delete_podpost(self, index):
+ self.podpostcache.delete(index)
+ self.store.delete(index)
+
def get_store(self):
"""
get the store
diff --git a/python/podcast/podcast.py b/python/podcast/podcast.py
index 9d771339c871afff7c3c06451dee60b3951d4b85..645acf7dac203481f747c9ac06953dfd03ba53f9 100644
--- a/python/podcast/podcast.py
+++ b/python/podcast/podcast.py
@@ -3,6 +3,9 @@ Podcast
"""
import sys
+from typing import List, Dict, Optional, Generator
+
+from podcast.factory import Factory
sys.path.append("/usr/share/podqast/python")
@@ -16,70 +19,156 @@ from email.utils import mktime_tz, parsedate_tz
import time
import hashlib
import os
+import logging
import feedparser
import pyotherside
+logger = logging.getLogger(__name__)
+
+
+class FeedFetchingError(Exception):
+ pass
+
class Podcast:
"""
A saved podcast
"""
- def __init__(self, url, feed=None):
+ alt_feeds: List[Dict[str, str]]
+ logo_path: Optional[str]
+ logo_url: str
+ description: str
+ title: Optional[str]
+ #: the ids of all entries of this feed from oldest [0] to newest [-1]
+ entry_ids_old_to_new: List[str]
+ url: str
+ version: int
+ subscribed: bool
+
+ def __init__(self, url, preview=False):
"""
The podcast itself only needs it's url
url: Url of the podcast
"""
+ self.subscribed = False
+ self.entry_ids_old_to_new = []
+ try:
+ self.__fetch_and_parse_initial(url, store_episodes=not preview)
+ except ValueError:
+ logger.exception("error while parsing")
+ return
+ except FeedFetchingError:
+ logger.exception("error while parsing")
+ return
+ self.move = -1
+ def __fetch_and_parse_initial(self, url, store_episodes=True):
+ logger.info("initializing podcast %s", url)
self.url = url
- if url.find("tumblr.com") >= 0:
- agent = util.user_agent2
- else:
- agent = util.user_agent
-
- self.feed = feedparser.parse(url, agent=agent)
-
- if self.feed.bozo != 0:
- exc = self.feed.bozo_exception
- if type(exc) != feedparser.CharacterEncodingOverride:
- pyotherside.send(
- "Podcast init: error in parsing feed" + str(type(exc))
- )
- self.title = None
- return
- pyotherside.send(
- "podcast init size of entries: %d" % len(self.feed.entries)
- )
-
+ feed = self._fetch(url)
try:
- self.title = self.feed.feed.title
+ self.title = feed.feed.title
except:
- pyotherside.send("podcast: no title")
- self.title = None
- return
-
- self.link = self.feed.feed.link
+ logger.exception("podcast: no title")
+ raise ValueError("Podcast has no name")
+ try:
+ self.link = feed.feed.link
+ except:
+ self.link = url
try:
- self.description = self.feed.feed.description
+ self.description = feed.feed.description
except:
self.description = ""
-
try:
- self.logo_url = self.feed.feed.image.href
- pyotherside.send("podcast logo_url: " + self.logo_url)
+ self.logo_url = feed.feed.image.href
+ logger.debug("podcast logo_url: %s", self.logo_url)
except:
self.logo_url = "../../images/podcast.png"
- pyotherside.send(
- "podcast logo_url error: " + str(sys.exc_info()[0])
- )
-
+ logger.exception("podcast logo_url error")
self.logo_path = None
+ self.__set_data_from_feed(feed)
+ if store_episodes:
+ self.__process_episodes(feed)
+ else:
+ self.__process_episodes(feed, limit=1, supress_limit_notification=True)
- if "updated_parsed" in self.feed:
- self.published = time.mktime(self.feed.updated_parsed)
- elif "Date" in self.feed.headers:
- self.published = mktime_tz(parsedate_tz(self.feed.headers["Date"]))
- self.move = -1
+ def __process_episodes(self, feed, limit=0, supress_limit_notification=False):
+ """
+ returns the episodes in order from newest to oldest
+ properly sets the entry_ids_old_to_new
+ """
+
+ logger.debug("fetching episodes for %s", self.title)
+
+ if self.logo_path:
+ image = self.logo_path
+ else:
+ image = self.logo_url
+
+ old_first_id = self.entry_ids_old_to_new[-1] if len(self.entry_ids_old_to_new) > 0 else ""
+
+ def entry_already_known(entry):
+ thehash = hashlib.sha256(entry.id.encode()).hexdigest()
+ logger.debug("current entry id %s", thehash)
+ logger.debug("old first id %s", old_first_id)
+ return thehash == old_first_id
+
+ try:
+ if self.autolimit:
+ limit = self.autolimit
+ except:
+ self.autolimit = None
+ all_episode_count = len(self.entry_ids_old_to_new)
+ new_posts = []
+ for entry in feed.entries:
+ if entry_already_known(entry):
+ pyotherside.send("refreshPost", None)
+ break
+ all_episode_count += 1
+ post = Podpost(entry, self.url, image)
+ post.save()
+ new_posts.append(post)
+ # todo: limit stuff probably broken
+ if limit != 0 and all_episode_count >= limit:
+ if not supress_limit_notification:
+ pyotherside.send("apperror", "Auto post limit reached for %s!" % self.title)
+ break
+ new_post_ids = [post.id for post in new_posts]
+ new_post_ids.reverse()
+ self.entry_ids_old_to_new.extend(new_post_ids)
+ self.save()
+ return new_posts
+
+ def __set_data_from_feed(self, feed):
+ logger.debug("setting instance values from feed")
+ self.__set_times_from_feeds(feed)
+ self._set_alt_feeds(feed)
+
+ def __set_times_from_feeds(self, feed):
+ if "updated_parsed" in feed:
+ self.published = time.mktime(feed.updated_parsed)
+ elif "Date" in feed.headers:
+ self.published = mktime_tz(parsedate_tz(feed.headers["Date"]))
+
+ def _fetch(self, url):
+ if url.find("tumblr.com") >= 0:
+ agent = util.user_agent2
+ else:
+ agent = util.user_agent
+ feed = feedparser.parse(url, agent=agent)
+ if feed.bozo != 0:
+ exc = feed.bozo_exception
+ if type(exc) != feedparser.CharacterEncodingOverride:
+ logger.warning(
+ "Podcast init: error in parsing feed %s", str(type(exc))
+ )
+ self.title = None
+ raise FeedFetchingError("error in feed")
+ logger.info(
+ "podcast init size of entries: %d", len(feed.entries)
+ )
+ return feed
def feedinfo(self):
"""
@@ -108,31 +197,23 @@ class Podcast:
"logo_url": image,
}
- def get_alt_feeds(self):
+ def _set_alt_feeds(self, feed):
"""
return all alternative feeds
"""
-
- altfeeds = []
-
- for feed in self.feed.feed.links:
- try:
- if (
- feed.type == "application/rss+xml"
- or feed.type == "application/atom+xml"
- ):
- if "title" in feed:
- title = feed.title
+ logger.info("setting alt_feeds")
+ self.alt_feeds = []
+ if "links" in feed:
+ for link in feed.links:
+ if link.type == "application/rss+xml" or link.type == "application/atom+xml":
+ if "title" in link:
+ title = link.title
else:
title = ""
+ self.alt_feeds.append({"url": link.href, "title": title})
+ logger.debug("setting alt feed: %s %s", title, link.href)
- altfeeds.append({"url": feed.href, "title": title})
- except:
- pass
-
- return {"altfeeds": altfeeds}
-
- def get_entry(self, id):
+ def get_entry(self, id) -> Podpost:
"""
Get an entry from podcast by id
"""
@@ -141,66 +222,24 @@ class Podcast:
entry = Factory().get_podpost(id)
return entry
- def get_entries(self):
+ def get_entries(self) -> Generator[dict, None, None]:
"""
Return a list of entries
"""
-
from podcast.factory import Factory
+ logger.debug("Podcast get_entries started.")
+ for id in self.entry_ids_old_to_new:
+ yield Factory().get_podpost(id).get_data()
- pyotherside.send("Podcast get_entries started.")
-
- entries = []
-
- if self.logo_path:
- image = self.logo_path
- else:
- image = self.logo_url
-
- for entry in self.feed.entries:
- post = Podpost(entry, self.url, image)
- pyotherside.send("pid: " + post.id)
- p2 = Factory().get_podpost(post.id)
- if p2:
- post = p2
- entries.append(post.get_data())
-
- pyotherside.send("Podcast get_entries ended.")
- return entries
-
- def get_first_entry(self):
+ def get_latest_entry(self) -> Optional[Podpost]:
"""
- Get the first entry from podcast
+ Get the newest entry from podcast
+ returns None if there are no entries
"""
-
- from podcast.factory import Factory
-
- if len(self.feed.entries) < 1:
+ if len(self.entry_ids_old_to_new) > 0:
+ return self.get_entry(self.entry_ids_old_to_new[-1])
+ else:
return None
- entry = Podpost(self.feed.entries[0], self.url, self.logo_url)
- p2 = Factory().get_podpost(entry.id)
- if p2:
- entry = p2
- Factory().podpostcache.store(entry.id, entry)
- return entry
-
- def get_last_entry(self):
- """
- Get the last entry from podcast
- """
-
- from podcast.factory import Factory
-
- entry = Podpost(
- self.feed.entries[len(self.feed.entries) - 1],
- self.url,
- self.logo_url,
- )
- p2 = Factory().get_podpost(entry.id)
- if p2:
- entry = p2
- Factory().podpostcache.store(entry.id, entry)
- return entry
def save(self):
"""
@@ -231,12 +270,12 @@ class Podcast:
rdot = self.logo_url.rfind(".")
extexcl = self.logo_url.rfind("?")
if extexcl > 0:
- ext = self.logo_url[rdot + 1 : extexcl]
+ ext = self.logo_url[rdot + 1: extexcl]
else:
- ext = self.logo_url[rdot + 1 :]
+ ext = self.logo_url[rdot + 1:]
if ext == "jpg" or ext == "jpeg" or ext == "png" or ext == "gif":
name = (
- hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext
+ hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext
)
logo_path = os.path.join(iconpath, name)
@@ -244,8 +283,8 @@ class Podcast:
util.dl_from_url(self.logo_url, logo_path)
except:
logo_path = ""
- pyotherside.send(
- "could not load icon: " + str(sys.exc_info()[0])
+ logger.info(
+ "could not load icon: %s", str(sys.exc_info()[0])
)
self.logo_path = logo_path
else:
@@ -255,82 +294,42 @@ class Podcast:
"""
Refresh podcast and return list of new entries
"""
-
pyotherside.send("refreshPost", self.title)
+ old_published = self.published
+ feed = self._fetch(self.url)
+ self.__set_data_from_feed(feed)
- feed = feedparser.parse(self.url)
- if feed.bozo != 0:
- exc = self.feed.bozo_exception
- if type(exc) != feedparser.CharacterEncodingOverride:
- pyotherside.send("refreshPost", None)
- return []
-
- if "updated_parsed" in feed:
- published = time.mktime(feed.updated_parsed)
- elif "Date" in feed.headers:
- published = mktime_tz(parsedate_tz(feed.headers["Date"]))
-
- first = self.get_first_entry()
- pyotherside.send("refresh published:", published, self.published)
- if published <= self.published:
+ logger.info("refresh published: %s %s", self.published, old_published)
+ if self.published <= old_published:
pyotherside.send("refreshPost", None)
return []
- self.feed = feed
- self.published = published
self.save()
- entries = []
- if self.logo_path:
- image = self.logo_path
- else:
- try:
- image = self.logo_url
- except:
- image = self.logo_url = "../../images/podcast.png"
-
- pyotherside.send("refresh first.id", first.id)
-
move = self.move
if move < 0:
move = moveto
- pyotherside.send("Moveto is %d" % move)
+ logger.debug("Moveto is %d", move)
- for entry in self.feed.entries:
- thehash = hashlib.sha256(entry.id.encode()).hexdigest()
- pyotherside.send("refresh entry.id", thehash)
- pyotherside.send("first.id", first.id)
- if thehash == first.id:
- pyotherside.send("refreshPost", None)
- break
-
- post = Podpost(entry, self.url, image)
- post.save()
- entries.append(
- {
- "id": post.id,
- "post": post.get_data(),
- "pctitle": self.title,
- "pstitle": post.title,
- "move": move,
- }
- )
- try:
- if self.autolimit:
- limit = self.autolimit
- except:
- self.autolimit = None
-
- if limit != 0 and len(entries) >= limit:
- pyotherside.send(
- "apperror", "Auto post limit reached for %s!" % self.title
- )
- break
-
- pyotherside.send("Fount %d new entries." % len(entries))
+ yield from self.__process_refreshed_feedentries(feed, limit, move)
pyotherside.send("refreshPost", None)
- return entries
+
+ def __process_refreshed_feedentries(self, feed, limit, move):
+ """
+ saves new entries for this feed
+ yields all new entries
+ """
+ new_posts = self.__process_episodes(feed, limit)
+ for post in new_posts:
+ yield {
+ "id": post.id,
+ "post": post.get_data(),
+ "pctitle": self.title,
+ "pstitle": post.title,
+ "move": move
+ }
+ logger.info("Fount %d new entries.", len(new_posts))
def set_params(self, params):
"""
@@ -375,6 +374,8 @@ class PodcastFactory(metaclass=Singleton):
"""
Helping Factory
"""
+ store: Store
+ podcastcache: Cache
def __init__(self, progname="harbour-podqast"):
"""
@@ -409,25 +410,34 @@ class PodcastFactory(metaclass=Singleton):
self.store = Store(storepath)
- def get_podcast(self, url):
+ def get_podcast(self, url, preview: bool = False) -> Optional[Podcast]:
"""
Get a new podcast instance with name url
"""
podcast = self.podcastcache.get(url)
if not podcast:
- pyotherside.send("PodcastFactory: not in cache")
+ logger.debug("PodcastFactory: not in cache %s", url)
podcast = self.store.get(url)
if podcast:
- pyotherside.send("PodcastFactory: From disk")
+ logger.debug("PodcastFactory: From disk %s", url)
else:
- podcast = Podcast(url)
+ podcast = Podcast(url, preview=preview)
if podcast.title:
- pyotherside.send("PodcastFactory: New instance")
+ logger.debug("PodcastFactory: New instance %s", url)
else:
return None
- pyotherside.send("PodcastFactory: store to cache")
+ logger.debug("PodcastFactory: store to cache %s", url)
self.podcastcache.store(url, podcast)
else:
- pyotherside.send("PodcastFactory: from cache")
+ logger.debug("PodcastFactory: from cache %s", url)
return podcast
+
+ def remove_podcast(self, url):
+ logger.info("deleting podcast and entries %s", url)
+ to_delete = self.get_podcast(url)
+ if to_delete != None:
+ for post in to_delete.entry_ids_old_to_new:
+ Factory().delete_podpost(post)
+ self.podcastcache.delete(url)
+ self.store.delete(url)
diff --git a/python/podcast/podcastlist.py b/python/podcast/podcastlist.py
index cdd3be47cef1dba7518268967692438f67401386..5fe15d623e6d0fac922cfc4454a9c730a95b195a 100644
--- a/python/podcast/podcastlist.py
+++ b/python/podcast/podcastlist.py
@@ -3,6 +3,7 @@ List of subscribed podcasts
"""
import sys
+from typing import List
sys.path.append("../")
@@ -12,6 +13,9 @@ from podcast.podcast import PodcastFactory
from podcast import util
from podcast import gpodder_import
import pyotherside
+import logging
+
+logger = logging.getLogger(__name__)
listname = "the_podcast_list"
@@ -20,6 +24,7 @@ class PodcastList:
"""
This element holds all podcasts
"""
+ podcasts: List[str]
def __init__(self):
"""
@@ -58,15 +63,14 @@ class PodcastList:
pc.delete()
self.save()
- def get_podcasts(self):
+ def get_podcasts(self) -> str:
"""
return the list of subscribed podcasts
"""
-
for podcast in self.podcasts:
pc = PodcastFactory().get_podcast(podcast)
if pc:
- if pc.subscribed:
+ if hasattr(pc, "subscribed") and pc.subscribed:
yield podcast
def refresh(self, moveto, limit=0):
@@ -84,8 +88,8 @@ class PodcastList:
for post in pc.refresh(moveto, limit):
yield post
except:
- pyotherside.send(
- "error", "podcast refresh failed for %s." % pc.title
+ logger.exception(
+ "podcast refresh failed for %s." % pc.title
)
count += 1
diff --git a/python/podcast/podpost.py b/python/podcast/podpost.py
index 3b24e5771b4fe245316de1e4012105383c634498..b13c9af556b43ee3c2746a7a74a54ffec5dcba51 100644
--- a/python/podcast/podpost.py
+++ b/python/podcast/podpost.py
@@ -11,6 +11,7 @@ import os
from urllib.parse import urlparse
import html2text
from email.utils import mktime_tz, parsedate_tz
+from typing import Optional, List
from podcast.factory import Factory
from podcast.util import s_to_hr, tx_to_s, dl_from_url, dl_from_url_progress
@@ -20,6 +21,9 @@ from podcast.queue import QueueFactory
import pyotherside
import time
import re
+import logging
+
+logger = logging.getLogger(__name__)
PLAY = 1
PAUSE = 2
@@ -30,6 +34,29 @@ class Podpost:
"""
One podcast post. It has all information needed for playing and information.
"""
+ file_path: Optional[str]
+ plainpart: Optional[str]
+ htmlpart: Optional[str]
+ title: str
+ percentage: float
+ position: int
+ isaudio: bool
+ id: str
+ state: int
+ favorite: bool
+ author: str
+ logo_url: str
+ logo_path: Optional[str]
+ href: str
+ duration: Optional[int]
+ length: int
+ published: Optional[float]
+ insert_date: object
+ link: Optional[str]
+ type: str
+ podurl: str
+ chapters: List
+ version: int
def __init__(self, entry, podurl, logo_url, data=None, isaudio=False):
"""
@@ -39,12 +66,16 @@ class Podpost:
entry: the entry took from feedparser
podcast: podcast url
"""
+ self.version = 1
self.isaudio = isaudio
self.position = 0
self.state = STOP
self.percentage = 0
self.favorite = False
+ self.length = 0
+ self.type = ""
+ self.href = ""
if isaudio:
self.title = data["title"]
@@ -78,7 +109,7 @@ class Podpost:
self.published = None
try:
if not self.published:
- pyotherside.send("no published_parsed")
+ logger.debug("no published_parsed")
self.published = mktime_tz(parsedate_tz(entry.published))
except:
self.published = 0
@@ -125,7 +156,7 @@ class Podpost:
if entry.id != "":
self.id = hashlib.sha256(entry.id.encode()).hexdigest()
else:
- pyotherside.send("podpost: empty id")
+ logger.debug("podpost: empty id")
self.id = hashlib.sha256(entry.summary.encode()).hexdigest()
else:
self.id = hashlib.sha256(entry.summary.encode()).hexdigest()
@@ -201,7 +232,7 @@ class Podpost:
queue = QueueFactory().get_queue()
if position:
self.position = position
- self.save()
+ self.save()
if self.duration:
if self.position > 0:
add = ""
@@ -244,36 +275,15 @@ class Podpost:
else:
asection = ""
adate = 0
- pyotherside.send("adate, asection", adate, asection)
- pyotherside.send("date, section", date, section)
+ logger.debug("adate, asection %s, %s", adate, asection)
+ logger.debug("date, section %s, %s", date, section)
if self.duration:
duration = s_to_hr(self.duration)
else:
duration = ""
- if self.link:
- link = self.link
- else:
- link = ""
-
- try:
- length = self.length
- except:
- length = 0
-
- try:
- favorite = self.favorite
- except:
- favorite = False
-
- try:
- isaudio = self.isaudio
- except:
- isaudio = self.isaudio = False
-
loaded = False
-
- if isaudio:
+ if self.isaudio:
try:
loaded = os.path.exists(self.href)
except:
@@ -284,13 +294,13 @@ class Podpost:
else:
haschapters = False
- pyotherside.send("loaded", loaded)
+ logger.debug("loaded %s", loaded)
return {
"id": self.id,
"title": self.title,
"url": self.podurl,
"logo_url": image,
- "link": link,
+ "link": self.link,
"description": self.plainpart,
"detail": self.htmlpart,
"timestring": timestring,
@@ -299,12 +309,12 @@ class Podpost:
"section": section,
"adate": adate,
"asection": asection,
- "length": length,
+ "length": self.length,
"duration": duration,
"dlperc": self.percentage,
- "favorite": favorite,
+ "favorite": self.favorite,
"inqueue": inqueue,
- "isaudio": isaudio,
+ "isaudio": self.isaudio,
"loaded": loaded,
"haschapters": haschapters,
}
@@ -314,12 +324,12 @@ class Podpost:
Save this element. If self.insert_date = None: set date
"""
- pyotherside.send("storing podpost %s" % self.id)
+ logger.info("storing podpost %s", self.id)
if not self.logo_path:
self.download_icon()
store = Factory().get_store()
store.store(self.id, self)
- pyotherside.send("...done")
+ logger.debug("...done")
def download_audio(self):
"""
@@ -333,7 +343,7 @@ class Podpost:
afilepath = Factory().afilepath
filename = os.path.basename(urlparse(self.href).path)
rdot = filename.rfind(".")
- ext = filename[rdot + 1 :]
+ ext = filename[rdot + 1:]
name = hashlib.sha256(self.href.encode()).hexdigest() + "." + ext
namepart = name + ".part"
@@ -379,10 +389,10 @@ class Podpost:
iconpath = Factory().iconpath
rdot = self.logo_url.rfind(".")
- ext = self.logo_url[rdot + 1 :]
+ ext = self.logo_url[rdot + 1:]
if ext == "jpg" or ext == "jpeg" or ext == "png" or ext == "gif":
name = (
- hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext
+ hashlib.sha256(self.logo_url.encode()).hexdigest() + "." + ext
)
logo_path = os.path.join(iconpath, name)
@@ -512,11 +522,11 @@ class Podpost:
def get_topath():
rdot = self.file_path.rfind(".")
- ext = self.file_path[rdot + 1 :]
+ ext = self.file_path[rdot + 1:]
tn = (
str(self.title.encode("ascii", "ignore"))
- .strip()
- .replace(" ", "_")[1:]
+ .strip()
+ .replace(" ", "_")[1:]
)
tnb = re.sub(r"(?u)[^-\w.]", "", tn) + "." + ext
tnb = "".join(x for x in tnb if x.isalnum() or x in "._-()")
@@ -526,7 +536,7 @@ class Podpost:
return topath
if self.favorite and do_download:
- pyotherside.send("need to download file")
+ logger.info("need to download file")
for perc in self.download_audio():
pyotherside.send("downloading", self.id, perc)
@@ -549,7 +559,7 @@ class Podpost:
try:
self.chapters[0].selected
except AttributeError:
- pyotherside.send("setting chapter selection flags")
+ logger.info("setting chapter selection flags")
for chapter in self.chapters:
chapter["selected"] = True
self.save()
@@ -577,3 +587,5 @@ class Podpost:
favorite = False
return favorite
+
+
diff --git a/qml/components/BackupButtons.qml b/qml/components/BackupButtons.qml
new file mode 100644
index 0000000000000000000000000000000000000000..e8a1befeb409a7293832b3bd956969e7a86e5ed4
--- /dev/null
+++ b/qml/components/BackupButtons.qml
@@ -0,0 +1,68 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+
+Column {
+ id: backupb
+ property bool backupRunning: false
+ property bool opmlWriteRunning: false
+
+ spacing: Theme.paddingMedium
+
+ Connections {
+ target: feedparserhandler
+ onBackupDone: {
+ console.log("Notify backup done")
+ appNotification.previewSummary = qsTr("Backup done")
+ appNotification.previewBody = qsTr("Backup done to ") + tarpath
+ appNotification.body = qsTr("Backup done to ") + tarpath
+ appNotification.publish()
+ backupRunning = false
+ }
+
+ onOpmlSaveDone: {
+ console.log("Notify opml save done")
+ appNotification.previewSummary = qsTr("OPML file saved")
+ appNotification.previewBody = qsTr("OPML file saved to ") + opmlpath
+ appNotification.body = qsTr("OPML file saved to ") + opmlpath
+ appNotification.publish()
+ opmlWriteRunning = false
+ }
+ }
+
+ Button {
+ id: importbutton
+ text: qsTr("Backup")
+ enabled: !backupRunning
+ width: parent.width
+ onClicked: {
+ Remorse.popupAction(migrationpage, qsTr("Start Backup"), function() {
+ backupRunning = true
+ feedparserhandler.doBackup()
+ })
+ }
+ BusyIndicator {
+ size: BusyIndicatorSize.Medium
+ anchors.centerIn: importbutton
+ running: backupRunning
+ }
+ }
+
+ Button {
+ id: opmlbutton
+ text: qsTr("Save to OPML")
+ enabled: !opmlWriteRunning
+ width: parent.width
+ onClicked: {
+ Remorse.popupAction(migrationpage, qsTr("Start OPML export"), function() {
+ opmlWriteRunning = true
+ feedparserhandler.doWriteOpml()
+ })
+ }
+
+ BusyIndicator {
+ size: BusyIndicatorSize.Medium
+ anchors.centerIn: parent
+ running: opmlWriteRunning
+ }
+ }
+}
diff --git a/qml/components/FeedParser.py b/qml/components/FeedParser.py
index 2763fe032563f55f3bc3e6c89a15188ba20ebc2a..39984ae32d21403487a7eb548e86c2bc0535b420 100644
--- a/qml/components/FeedParser.py
+++ b/qml/components/FeedParser.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import logging
import pyotherside
import threading
@@ -18,40 +19,37 @@ from podcast.inbox import InboxFactory
from podcast.factory import Factory
from podcast.util import create_opml
+logger = logging.getLogger(__name__)
+
cachevar = {}
-def get_feedinfo(url):
- podcast = PodcastFactory().get_podcast(url)
+def get_feedinfo(url, preview=False):
+ logger.info("getting feedinfo %s for %s", "in previewmode" if preview else "", url)
+ podcast = PodcastFactory().get_podcast(url, preview=preview)
+ logger.info("fetched preview, sending feedinfo now.")
pyotherside.send("feedinfo", podcast.feedinfo())
def get_alternatives(url):
podcast = PodcastFactory().get_podcast(url)
- pyotherside.send("alternatives", podcast.get_alt_feeds())
+ pyotherside.send("alternatives", {"altfeeds": podcast.alt_feeds})
-def get_first_entry(url):
+def get_latest_entry(url):
"""
Get first Podcast entry
"""
podcast = PodcastFactory().get_podcast(url)
- entry = podcast.get_first_entry()
- data = entry.get_data()
- pyotherside.send("fevent", data)
-
+ entry = podcast.get_latest_entry()
+ if entry != None:
+ data = entry.get_data()
+ pyotherside.send("firstentryevent", data)
-def get_last_entry(url):
-
- podcast = PodcastFactory().get_podcast(url)
- entry = podcast.get_last_entry()
- data = entry.get_data()
- pyotherside.send("levent", data)
-
-def get_object(object):
- Factory().get_object(object)
+def init_from_qml(object):
+ Factory().init_from_qml(object)
def get_entries(url):
@@ -61,15 +59,9 @@ def get_entries(url):
podcast = PodcastFactory().get_podcast(url)
- feeds = []
-
- for feed_data in podcast.get_entries():
- feeds.append(feed_data)
+ feeds = list(podcast.get_entries())
- try:
- link = podcast.link
- except:
- link = podcast.link = podcast.feed.feed.link
+ link = podcast.link
feeds.sort(key=lambda r: r["date"], reverse=True)
pyotherside.send("createPodpostsList", feeds, podcast.title, link)
@@ -79,7 +71,7 @@ def subscribe_podcast(url):
"""
Subscribe a podcast
"""
-
+ PodcastFactory().remove_podcast(url)
podcast_list = PodcastListFactory().get_podcast_list()
podcast_list.add(url)
pyotherside.send("subscribed", url)
@@ -315,7 +307,13 @@ class FeedParser:
def getfeedinfo(self, theurl):
if self.bgthread.is_alive():
return
- self.bgthread = threading.Thread(target=get_feedinfo, args=[theurl])
+ self.bgthread = threading.Thread(target=get_feedinfo, args=[theurl, False])
+ self.bgthread.start()
+
+ def getpodcastpreview(self, theurl):
+ if self.bgthread.is_alive():
+ return
+ self.bgthread = threading.Thread(target=get_feedinfo, args=[theurl, True])
self.bgthread.start()
def getalternatives(self, theurl):
@@ -326,10 +324,10 @@ class FeedParser:
)
self.bgthread.start()
- def getfirstentry(self, theurl):
+ def getlatestentry(self, theurl):
if self.bgthread.is_alive():
return
- self.bgthread = threading.Thread(target=get_first_entry, args=[theurl])
+ self.bgthread = threading.Thread(target=get_latest_entry, args=[theurl])
self.bgthread.start()
def getentries(self, theurl):
@@ -338,12 +336,6 @@ class FeedParser:
self.bgthread = threading.Thread(target=get_entries, args=[theurl])
self.bgthread.start()
- def getlastentry(self, theurl):
- if self.bgthread.is_alive():
- return
- self.bgthread = threading.Thread(target=get_last_entry, args=[theurl])
- self.bgthread.start()
-
def getpodcasts(self):
if self.bgthread.is_alive():
return
diff --git a/qml/components/FeedParserPython.qml b/qml/components/FeedParserPython.qml
index 40ab69a0fb7c39044f06fb700e4997c3d9bf754f..46b7782aeb1d951e5451f279b749704a759f5e06 100644
--- a/qml/components/FeedParserPython.qml
+++ b/qml/components/FeedParserPython.qml
@@ -6,8 +6,7 @@ Python {
id: feedparserhandler
signal feedInfo(var pcdata)
- signal firstEvent(var pcdata)
- signal lastEvent(var pcdata)
+ signal latestEvent(var pcdata)
signal alternatives(var pcdata)
signal podcastsList(var pcdata)
signal createPodpostsList(var pcdata, string podtitle, string podlink)
@@ -25,8 +24,7 @@ Python {
Component.onCompleted: {
setHandler("feedinfo", feedInfo)
- setHandler("fevent", firstEvent)
- setHandler("levent", lastEvent)
+ setHandler("firstentryevent", latestEvent)
setHandler("alternatives", alternatives)
setHandler("podcastslist", podcastsList)
setHandler("createPodpostsList", createPodpostsList)
@@ -44,22 +42,27 @@ Python {
addImportPath(Qt.resolvedUrl('.'));
importModule('FeedParser', function () {
- console.log('FeedParser is now imported')
- call("FeedParser.get_object", [podqast], function() {});
+ console.log('FeedParser is now imported');
+ call("FeedParser.init_from_qml", [podqast], function() {});
});
}
+
function getPodcast(url) {
console.log("url: " + url)
call("FeedParser.feedparse.getfeedinfo", [url], function() {});
- // call("FeedParser.get_feedinfo", [url], function() {});
+ }
+
+ function getPodcastPreview(url) {
+ console.log("fetching preview for " + url)
+ call("FeedParser.feedparse.getpodcastpreview", [url], function() {});
}
function getPodcasts() {
call("FeedParser.feedparse.getpodcasts", function() {});
// call("FeedParser.get_podcasts", function() {});
}
+
function getPodcastsPreCache() {
call("FeedParser.feedparse.getpodcastscache", function() {});
- // call("FeedParser.get_podcastscache", function() {});
}
function getEntries(url) {
@@ -67,18 +70,11 @@ Python {
call("FeedParser.feedparse.getentries", [url], function() {});
// call("FeedParser.get_entries", [url], function() {});
}
- function getFirstEvent(url) {
- console.log("getFirstElement")
- call("FeedParser.feedparse.getfirstentry", [url], function() {});
- // call("FeedParser.get_first_entry", [url], function() {});
+ function getLatestEvent(url) {
+ console.log("getLatestEvent from feed " + url)
+ call("FeedParser.feedparse.getlatestentry", [url], function() {});
}
- function getLastEvent(url) {
- console.log("getLastElement")
- console.log(url)
- call("FeedParser.feedparse.getlastentry", [url], function() {});
- // call("FeedParser.get_last_entry", [url], function() {});
- }
function getAlternatives(url) {
call("FeedParser.feedparse.getalternatives", [url], function() {})
@@ -172,9 +168,6 @@ Python {
console.log('python error: ' + traceback);
}
- onReceived: {
- console.log('got message from python: ' + data);
- }
onAppError: {
console.log("Notify error " + errmessage)
appNotification.previewSummary = qsTr("Error")
diff --git a/qml/components/MigrationHandler.py b/qml/components/MigrationHandler.py
new file mode 100644
index 0000000000000000000000000000000000000000..660a9b355cd0e3d6f0155f4e22a975bfd235408d
--- /dev/null
+++ b/qml/components/MigrationHandler.py
@@ -0,0 +1,10 @@
+import sys
+sys.path.append("/usr/share/harbour-podqast/python")
+
+from podcast import data_migration
+
+def needs_migration():
+ return data_migration.needs_migration()
+
+def run_migrations():
+ data_migration.run_migrations()
\ No newline at end of file
diff --git a/qml/components/MigrationHandler.qml b/qml/components/MigrationHandler.qml
new file mode 100644
index 0000000000000000000000000000000000000000..9e49d73947031daee7251742ccbf9e4b7d802f61
--- /dev/null
+++ b/qml/components/MigrationHandler.qml
@@ -0,0 +1,42 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import io.thp.pyotherside 1.4
+
+Python {
+ id: migrationhandler
+
+ signal migrationNeeded(bool is_needed)
+ signal migrationDone()
+ signal migrationStart(int miglen)
+ signal migrationProgress(int migpos)
+
+ property int progress_max: 1
+ property int progress_current: 1
+ property double progress: progress_current/progress_max
+ property bool running: false
+
+ Component.onCompleted: {
+ setHandler("migrationStart", migrationStart)
+ setHandler("migrationProgress", migrationProgress)
+ setHandler("migrationDone", migrationDone)
+
+ addImportPath(Qt.resolvedUrl('.'));
+ importModule('MigrationHandler', function () {
+ console.log("MigrationHandler is now imported")
+ call('MigrationHandler.needs_migration', [], function(is_needed){
+ console.log("Migration is needed: " + is_needed)
+ migrationNeeded(is_needed)
+ })
+ })
+ }
+
+ function start_migration(){
+ call('MigrationHandler.run_migrations', function(){})
+ }
+
+ function migration_ended(){
+ console.log("migration done")
+ running = false
+ migrationDone()
+ }
+}
diff --git a/qml/components/QueueHandlerPython.qml b/qml/components/QueueHandlerPython.qml
index fd06ae6631516894d9a9fdd45e8087426a6c056b..1bb53a408220c93abced4311a421df4eca9981bd 100644
--- a/qml/components/QueueHandlerPython.qml
+++ b/qml/components/QueueHandlerPython.qml
@@ -109,9 +109,6 @@ Python {
console.log('python error: ' + traceback);
}
- onReceived: {
- console.log('got message from python: ' + data);
- }
onDoStop: {
console.log("stopped")
playerHandler.stop()
diff --git a/qml/harbour-podqast.qml b/qml/harbour-podqast.qml
index 9cbf181684b0f2a9ede8244b65e84edecbadd88b..e48cac004e1d57b77c93baada60999bd511f56d2 100644
--- a/qml/harbour-podqast.qml
+++ b/qml/harbour-podqast.qml
@@ -16,13 +16,30 @@ ApplicationWindow
property bool doWizzard: doWizzardConf.value
// property bool doWizzard: true
- initialPage: {
- var pagename
- if (podqast.doWizzard === true)
- pagename = "pages/Wizzard1.qml"
- else
- pagename = "pages/Queue.qml"
- return Qt.resolvedUrl(pagename)
+ // postponed until migrationhandler answers
+ initialPage: Page {
+ BusyIndicator {
+ size: BusyIndicatorSize.Large
+ anchors.centerIn: parent
+ running: true
+ }
+ }
+
+ MigrationHandler{
+ id: migrationhandler
+ onMigrationNeeded:
+ if(!is_needed){
+ var pagename
+ if (podqast.doWizzard === true)
+ pagename = "pages/Wizzard1.qml"
+ else
+ pagename = "pages/Queue.qml"
+ console.log("Opening " + pagename + " as first page")
+ pageStack.replace( Qt.resolvedUrl(pagename))
+ } else {
+ console.log("Opening migrationpage")
+ pageStack.replace( Qt.resolvedUrl("pages/DataMigration.qml"))
+ }
}
diff --git a/qml/pages/Archive.qml b/qml/pages/Archive.qml
index 7d36fa7104914adfc9be04bbe5c0e8d3d33ae897..1f957b59762c77a57ef1c96995237089cf61a050 100644
--- a/qml/pages/Archive.qml
+++ b/qml/pages/Archive.qml
@@ -120,7 +120,6 @@ Page {
topage: "External.qml"})
}
for (var i = 0; i < pcdata.length; i++) {
- console.log(pcdata[i])
pcdata[i]["bethead"] = "Podcast"
podcastsModel.append(pcdata[i]);
}
diff --git a/qml/pages/DataMigration.qml b/qml/pages/DataMigration.qml
new file mode 100644
index 0000000000000000000000000000000000000000..7f6daedb2cc732f231b186dba6c8ec4d82c01de0
--- /dev/null
+++ b/qml/pages/DataMigration.qml
@@ -0,0 +1,115 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import "../components"
+
+Page {
+
+ id: migrationpage
+ property bool running: false
+ property bool errorHappened: false
+ property var errorText : ""
+
+ Connections{
+ target: migrationhandler
+ onMigrationDone: {
+ appNotification.previewSummary = qsTr("Update done")
+ appNotification.previewBody = qsTr("Successfully migrated data to the current version")
+ appNotification.body = qsTr("Successfully migrated data to the current version")
+ appNotification.publish()
+ pageStack.replace( Qt.resolvedUrl("Queue.qml"))
+ }
+ onMigrationStart: {
+ console.log("migration started with steps: " + miglen)
+ running = true
+ migbar.maximumValue = miglen
+ migbar.value = 1
+ }
+
+ onMigrationProgress: {
+ console.log("migration progressed to " + migpos)
+ migbar.value = migpos
+ }
+
+ onError: {
+ errorHappened = true
+ errorText = traceback
+ }
+ }
+
+ Column {
+ id: migrationcomponent
+ width: parent.width
+ clip: true
+ spacing: Theme.paddingLarge
+ visible: !errorHappened
+ anchors {
+ fill: parent
+ leftMargin: Theme.horizontalPageMargin
+ rightMargin: Theme.horizontalPageMargin
+ }
+ PageHeader{
+ title: qsTr("Data Migration")
+ }
+
+ Label {
+ width: parent.width
+ wrapMode: Text.WordWrap
+ text: qsTr("Because the data format changed, podqast will need to migrate your data to the new format. You can use one of the buttons below to create a backup of your data for recovery, if anything goes wrong.")
+ }
+
+ Button {
+ text: qsTr("Start Migration")
+ onClicked: migrationhandler.start_migration()
+ visible: !running
+ enabled: !backupcomponent.backupRunning && !backupcomponent.opmlWriteRunning
+ width: parent.width
+ backgroundColor: Theme.highlightBackgroundColor
+ }
+
+ ProgressBar {
+ id: migbar
+ width: parent.width
+ height: Theme.itemSizeSmall
+ minimumValue: 1
+ visible: running
+ }
+
+ Item {
+ height: Theme.paddingMedium
+ width: parent.width
+ }
+
+ BackupButtons {
+ id: backupcomponent
+ visible: !running
+ width: parent.width
+ }
+ }
+
+ Column {
+ visible: errorHappened
+ spacing: Theme.paddingLarge
+ anchors {
+ fill: parent
+ leftMargin: Theme.horizontalPageMargin
+ rightMargin: Theme.horizontalPageMargin
+ }
+
+ Label{
+ text: qsTr("An error has happened.")
+ }
+
+ Button {
+ text: qsTr("Copy to clipboard")
+ onClicked: Clipboard.text = errorText
+ }
+
+ TextArea {
+ id: errortextarea
+ readOnly: true
+ wrapMode: Text.Wrap
+ text: errorText
+ width: parent.width
+ }
+ }
+}
diff --git a/qml/pages/DiscoverExport.qml b/qml/pages/DiscoverExport.qml
index 6894a357b84aa1db025015763044562a6504f675..befc3c039e0f449aac21f045661c2c77ee2e88cf 100644
--- a/qml/pages/DiscoverExport.qml
+++ b/qml/pages/DiscoverExport.qml
@@ -9,94 +9,31 @@ Page {
allowedOrientations: Orientation.All
property int margin: Theme.paddingMedium
-
SilicaFlickable {
id: mainflick
anchors.fill: parent
- // contentHeight: page.height
-
- property bool backupRunning: false
- property bool opmlWriteRunning: false
-
- Connections {
- target: feedparserhandler
- onBackupDone: {
- console.log("Notify backup done")
- appNotification.previewSummary = qsTr("Backup done")
- appNotification.previewBody = qsTr("Backup done to ") + tarpath
- appNotification.body = qsTr("Backup done to ") + tarpath
- appNotification.publish()
- mainflick.backupRunning = false
- }
- onOpmlSaveDone: {
- console.log("Notify opml save done")
- appNotification.previewSummary = qsTr("OPML file saved")
- appNotification.previewBody = qsTr("OPML file saved to ") + opmlpath
- appNotification.body = qsTr("OPML file saved to ") + opmlpath
- appNotification.publish()
- mainflick.opmlWriteRunning = false
- }
- }
AppMenu { thispage: "Backup" }
PrefAboutMenu {}
- Row {
- id: discovertitle
+ Column {
+ id: column
width: page.width
- Column {
- id: column
- width: page.width
- spacing: Theme.paddingLarge
+ spacing: Theme.paddingLarge
- PageHeader {
- title: qsTr("Export")
- }
+ PageHeader {
+ title: qsTr("Export")
}
- }
- Column {
- id: backupb
- height: Theme.itemSizeLarge
- anchors.top: discovertitle.bottom
- visible: experimentalConf.value
- Button {
- id: importbutton
- text: qsTr("Backup")
- enabled: !mainflick.backupRunning
- onClicked: {
- Remorse.popupAction(page, qsTr("Start Backup"), function() {
- mainflick.backupRunning = true
- feedparserhandler.doBackup()
- })
- }
- BusyIndicator {
- size: BusyIndicatorSize.Medium
- anchors.centerIn: parent
- running: mainflick.backupRunning
- }
- }
- }
- Column {
- id: opmlwrite
- height: Theme.itemSizeLarge
- anchors.top: backupb.bottom
- Button {
- id: opmlbutton
- text: qsTr("Save to OPML")
- enabled: !mainflick.opmlWriteRunning
- onClicked: {
- Remorse.popupAction(page, qsTr("Start OPML export"), function() {
- mainflick.opmlWriteRunning = true
- feedparserhandler.doWriteOpml()
- })
- }
- BusyIndicator {
- size: BusyIndicatorSize.Medium
- anchors.centerIn: parent
- running: mainflick.opmlWriteRunning
- }
+
+ BackupButtons {
+ width: page.width
+ anchors {
+ leftMargin: Theme.horizontalPageMargin
+ rightMargin: Theme.horizontalPageMargin
+ }
}
}
+
PlayDockedPanel { }
}
}
diff --git a/qml/pages/Podcast.qml b/qml/pages/Podcast.qml
index 7082bc15bd301d28dd8c89469b725dc57d91640c..2b480e3691159a866086cf9e8c2c5836fc39c10c 100644
--- a/qml/pages/Podcast.qml
+++ b/qml/pages/Podcast.qml
@@ -35,37 +35,27 @@ Page {
PrefAboutMenu {}
Component.onCompleted: {
- feedparserhandler.getPodcast(url)
+ feedparserhandler.getPodcastPreview(url)
}
Connections {
target: feedparserhandler
onFeedInfo: {
- console.log(pcdata.title)
- console.log(pcdata.logo_url)
- console.log(pcdata.description)
- console.log(pcdata.descriptionfull)
+ console.debug(pcdata.title)
+ console.debug(pcdata.logo_url)
+ console.debug(pcdata.description)
+ console.debug(pcdata.descriptionfull)
podimage.source=pcdata.logo_url
poddescription.text=pcdata.description
description=pcdata.descriptionfull
titlefull=pcdata.title
- console.log(description)
+ console.debug(description)
subscribebutton.enabled=true
feedparserhandler.getAlternatives(url)
}
- onFirstEvent: {
- console.log("fevent");
- console.log(pcdata.title)
- fpostlabel.visible = true
- fentryListModel.clear();
- fentryListModel.append(pcdata)
- feedparserhandler.getLastEvent(url)
- }
-
- onLastEvent: {
- console.log("levent");
+ onLatestEvent: {
console.log(pcdata.title)
lpostlabel.visible = true
lentryListModel.clear();
@@ -93,7 +83,7 @@ Page {
}
altcombo.visible=true
}
- feedparserhandler.getFirstEvent(url)
+ feedparserhandler.getLatestEvent(url)
}
onSubscribed: {
subscribebutton.subscribed = true
@@ -200,7 +190,7 @@ Page {
BusyIndicator {
size: BusyIndicatorSize.Medium
anchors.centerIn: subscribebutton
- running: subscribebutton.subscribing
+ running: subscribebutton.subscribing || !subscribebutton.enabled
}
}
ComboBox {
@@ -237,29 +227,6 @@ Page {
width: parent.width
height: Theme.itemSizeExtraLarge * 1.1
- model: ListModel {
- id: fentryListModel
- }
- delegate: PodcastEntryItem {}
- }
- Row {
- id: fpostlabel
- height: Theme.itemSizeExtraSmall
- anchors.top: fpodentries.bottom
- visible: false
- Label {
- anchors.verticalCenter: parent.verticalCenter
- font.pixelSize: Theme.fontSizeSmall
- color: Theme.highlightColor
- text: qsTr("First Post")
- }
- }
- SilicaListView {
- id: lpodentries
- anchors.top: fpostlabel.bottom
- width: parent.width
- height: Theme.itemSizeExtraLarge * 1.1
-
model: ListModel {
id: lentryListModel
}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b5399aaba2b1bc44e59015f9f35e75f3519e92cf
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+httpretty
+pytest
\ No newline at end of file
diff --git a/test/test_archive.py b/test/test_archive.py
index 758e5320cc6177783cc87bf1e9bc67df799a353b..e3d187d7f2efd82cadb3c001a724fb73f20384c0 100644
--- a/test/test_archive.py
+++ b/test/test_archive.py
@@ -3,6 +3,12 @@ test the archive
"""
import sys
+
+import httpretty
+from httpretty import HTTPretty, httprettified
+
+from test.test_podcast import read_testdata, xml_headers
+
sys.path.append("../python")
from podcast.archive import ArchiveFactory, Archive
@@ -30,17 +36,20 @@ def test_archive_save():
a = ArchiveFactory().get_archive()
a.save()
+@httprettified
def test_insert():
"""
"""
+ HTTPretty.register_uri(HTTPretty.GET, 'https://freakshow.fm/feed/opus/',
+ body=read_testdata('testdata/freakshow.rss'), adding_headers=xml_headers)
a = ArchiveFactory().get_archive()
p = Podcast('https://freakshow.fm/feed/opus/')
- e = p.get_first_entry()
+ e = p.get_entry(p.entry_ids_old_to_new[0])
e.save()
a.insert(e.id)
- e2 = p.get_last_entry()
+ e2 = p.get_entry(p.entry_ids_old_to_new[1])
e2.save()
a.insert(e2.id)
@@ -58,5 +67,5 @@ def test_get_archives():
assert type(post) == str
assert type(Factory().get_podpost(post)) == Podpost
count += 1
-
+ # make sure to clean ~/.local/share/harbour-podqast if this fails
assert count == 2
diff --git a/test/test_migration_to_v1.py b/test/test_migration_to_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f9e587c1fe064fb3c8b1cb746b5c2a6ce2f9509
--- /dev/null
+++ b/test/test_migration_to_v1.py
@@ -0,0 +1,41 @@
+import sys
+import os
+import tempfile
+from distutils.dir_util import copy_tree
+
+from podcast import data_migration
+from podcast.archive import ArchiveFactory
+from podcast.external import ExternalFactory
+from podcast.factory import Factory
+from podcast.inbox import InboxFactory
+from podcast.podcast import Podcast, PodcastFactory
+from podcast.podcastlist import PodcastListFactory
+from podcast.queue import QueueFactory
+
+sys.path.append("../python")
+
+def test_migration():
+ with tempfile.TemporaryDirectory() as tmpdir:
+ copy_tree(os.path.join(os.path.dirname(__file__), "testdata/migrationtests_v1/"), tmpdir)
+ os.environ["PODQAST_HOME"] = tmpdir
+ resetSingletons()
+ data_migration.run_migrations()
+ assert os.path.exists(os.path.join(tmpdir, "dataversion"))
+ podcasts = list(PodcastListFactory().get_podcast_list().get_podcasts())
+ assert len(podcasts) == 1
+ podcast:Podcast = PodcastFactory().get_podcast(podcasts[0])
+ assert podcast != None
+ assert len(podcast.entry_ids_old_to_new) == 22
+ assert len(list(podcast.get_entries())) == 22
+ del os.environ["PODQAST_HOME"]
+ resetSingletons()
+
+
+def resetSingletons():
+ Factory._instances = {}
+ PodcastListFactory._instances = {}
+ PodcastFactory._instances = {}
+ QueueFactory._instances = {}
+ ArchiveFactory._instances = {}
+ ExternalFactory._instances = {}
+ InboxFactory._instances = {}
diff --git a/test/test_podcast.py b/test/test_podcast.py
index e955d017873deacbcb479b5f03dfb47e49688e54..23dda8c35e72bb315c6daa986b75b92885f1c328 100644
--- a/test/test_podcast.py
+++ b/test/test_podcast.py
@@ -2,33 +2,59 @@
Test Podcast functions
"""
import sys
+import os
+
+import httpretty
+from httpretty import HTTPretty
+
sys.path.append("../python")
from podcast.podcast import Podcast
-from podcast.podpost import Podpost
+xml_headers = {"content-type": "application/rss+xml; charset=UTF-8"}
+def read_testdata(filename):
+ data = ""
+ with open(os.path.join(os.path.dirname(__file__), filename), 'r') as file:
+ data = file.read()
+ return data
+
+@httpretty.activate
def test_feed_entry():
"""
Get a feed entry
"""
+ HTTPretty.register_uri(HTTPretty.GET, "http://feeds.twit.tv/sn.xml",
+ body=read_testdata('testdata/twittv.xml'),adding_headers=xml_headers)
+ HTTPretty.register_uri(HTTPretty.GET, 'https://freakshow.fm/feed/opus/',
+ body=read_testdata('testdata/freakshow.rss'), adding_headers=xml_headers)
- for e in ['https://freakshow.fm/feed/opus',
- 'http://feeds.twit.tv/sn.xml',
- 'http://www.bbc.co.uk/programmes/p02nrspf/episodes/downloads.rss']:
- p = Podcast(e)
- e = p.get_first_entry()
- if e != None:
- assert type(e) == Podpost
-
- d = e.get_data()
- assert 'id' in d.keys()
- assert 'logo_url' in d.keys()
- assert 'description' in d.keys()
- print(d['id'], d['title'])
- print(d['logo_url'], d['description'])
- assert len(d['id']) > 0
- assert type(d['id']) == str
- assert len(d['logo_url']) > 0 or d['logo_url'] == ""
- assert type(d['logo_url']) == str
- e2 = p.get_entry(d['id'])
- assert e == e2
+ for data in [ 'https://freakshow.fm/feed/opus/',
+ 'http://feeds.twit.tv/sn.xml',
+ ]:
+ podcast = Podcast(data)
+ assert podcast.alt_feeds != None
+ for feed in podcast.alt_feeds:
+ assert type(feed['url']) == str
+ assert type(feed['title']) == str
+ feedinfo = podcast.feedinfo()
+ assert type(feedinfo["url"]) == str
+ assert type(feedinfo["title"]) == str
+ assert type(feedinfo["description"]) == str
+ assert type(feedinfo["descriptionfull"]) == str
+ assert type(feedinfo["logo_url"]) == str
+ assert podcast.get_latest_entry() != None
+ entries = podcast.get_entries()
+ for data in entries:
+ assert data != None
+ assert type(data) == dict
+ assert 'id' in data.keys()
+ assert 'logo_url' in data.keys()
+ assert 'description' in data.keys()
+ print(data['id'], data['title'])
+ print(data['logo_url'], data['description'])
+ assert len(data['id']) > 0
+ assert type(data['id']) == str
+ assert len(data['logo_url']) > 0 or data['logo_url'] == ""
+ assert type(data['logo_url']) == str
+ assert podcast.get_entry(data['id']) != None
+ assert data == podcast.get_entry(data['id']).get_data()
diff --git a/test/test_podpost.py b/test/test_podpost.py
index e16675eddb6bcb364748062e801c060dba0349b7..2d40f3ce8734e07515f4c66f3eb931eba3f2cc4f 100644
--- a/test/test_podpost.py
+++ b/test/test_podpost.py
@@ -17,7 +17,7 @@ def test_podpost_save():
f1 = Factory()
p = Podcast('https://freakshow.fm/feed/opus/')
- e = p.get_first_entry()
+ e = p.get_latest_entry()
id1 = e.id
e.save()
n = Factory().get_store().get(e.id)
diff --git a/test/test_queue.py b/test/test_queue.py
index 140a6792ea17243a8230e2021538aa14e6662920..76343369e664089900482ce9b5d256e6e1511b38 100644
--- a/test/test_queue.py
+++ b/test/test_queue.py
@@ -3,6 +3,7 @@ test the queue
"""
import sys
+
sys.path.append("../python")
from podcast.queue import QueueFactory
@@ -12,6 +13,7 @@ from podcast.podcast import Podcast
from podcast.podpost import Podpost
import podcast
+
def test_create_queue():
"""
def if a queue is created
@@ -19,10 +21,11 @@ def test_create_queue():
q1 = QueueFactory().get_queue()
assert type(q1) == Queue
-
+
q2 = QueueFactory().get_queue()
assert q1 == q2
+
def test_queue_save():
"""
does the queue save itself?
@@ -31,6 +34,7 @@ def test_queue_save():
q = QueueFactory().get_queue()
q.save()
+
def test_insert_top():
"""
"""
@@ -38,8 +42,8 @@ def test_insert_top():
q = QueueFactory().get_queue()
l1 = len(q.podposts)
p = Podcast('https://freakshow.fm/feed/opus/')
- e = p.get_first_entry()
- e2 = p.get_last_entry()
+ e = p.get_entry(p.entry_ids_old_to_new[1])
+ e2 = p.get_entry(p.entry_ids_old_to_new[0])
e.save()
q.insert_top(e.id)
@@ -53,16 +57,17 @@ def test_insert_top():
q.save()
+
def test_get_podposts():
"""
Test listing of podposts
"""
- q = QueueFactory().get_queue()
- count = 0
- for post in q.get_podposts():
+ queue = QueueFactory().get_queue()
+ queue.podposts = ['85140fc8e16ba62517877ef4857a85f5bbf83e6a236c659a73489f093e51f154',
+ 'ce656a3888a282fe9df727e0729fa7d375704e4bd6c0d50e9c0fe7ddafa232ec']
+ postids = list(queue.get_podposts())
+ assert len(postids) == 2
+ for post in postids:
assert type(post) == str
assert type(Factory().get_podpost(post)) == Podpost
- count += 1
-
- assert count == 2
diff --git a/test/testdata/freakshow.rss b/test/testdata/freakshow.rss
new file mode 100644
index 0000000000000000000000000000000000000000..c81a8fccf91a314a6c96eb557bb53cdfbf2f2e2f
--- /dev/null
+++ b/test/testdata/freakshow.rss
@@ -0,0 +1,15599 @@
+
+
+
+
+Schreibe uns einen Kommentar
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Podcast Clients — 2020 Technik-Rückblick — LiDAR — iPhone 12 — M1 — OpenZFS — NAS — Mac Apps — Bildschirme
+ +Das Jahr geht (endlich) zu Ende und wir füllen die Bits eines Bytes bis auf letzte Stelle. Ausgabe Nummer Zweihundertfünfundfünzig versucht Euch zu Weihnachten noch mal das Herz zu fluten.
+ +Dauer: 3:56:56
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Talking Heads — Konten und Banken — M1 und Wintel — Internet Tips — pretix — venueless — pretalx — Virtuelle Events — Videotechnik — Mikrofone und Headsets — Licht — Ethernet
+ +Heute dreht sich alles um Events. Wir begrüßen rami als Gast in der Sendung, der das Projekt pretix losgetreten hat, dass ein Ticketing-System für Veranstaltungen aller Art ist. Wir sprechen über Online-Events und in was von der Pandemie-Zeit übrig bleiben wird, wenn wir uns wieder in der Realität treffen können. Dazu gibt es noch eine Reihe von Tips für Technik zur Durchführung von Audio- und Videosessions.
+ +Dauer: 3:30:42
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++rami + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Ultraschall — M1 Macs — Big Sur
+ +Nach zwei Wochen und vor allem nachdem manche von uns bereits ein paar der neuen M1 Macs erhalten habe dreht sich hier ein weiteres Mal alles um die neuen Computer. Wir teilen erste Erfahrungen und Einschätzungen und sind überhaupt sehr aufgeregt. Außerdem ist Ralf zu Gast und berichtet von den Fortschritten beim Ultraschall-Projekt.
+ +Dauer: 3:41:53
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Dominik Wagner + | +
+
+
+ |
++ | +
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Ralf Stockmann + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Kontowechsel — Pausenende — Elektrourlaub — Tesla Battery Day — iPhone 12 — Apple Watch — iOS 14 und AirPods Pro — Augmented Reality — Big Sur — ARM-Macs — 5G — Audio auf dem Mac — Audiointerfaces — Küchenkram
+ +We are back. Nach einer längeren, im Kern ungeplanten aber trotzdem notwenidgen Verschnaufpause ist die Freak Show wieder am Start, denn wir mussten dringend mal wieder mehr über Macs reden. Wir gehen die neue Produkten von Apple der letzten Woche durch und natürlich legen wir einen Schwerpunkt auf die neuen ARM-Macs, die nun endlich vorgestellt worden. Dazu ein wenig Corona-Küchenkram rund um Waffeleisen und scharfe Messer.
+ +Dauer: 3:50:53
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Dominik Wagner + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Eine Spezialausgabe der Freak Show mit dem Schwerpunkt Elektromobilität
+ +Wir haben schon oft darüber gesprochen, aber so richtig Ahnung haben wir ja eh nicht. Deswegen haben wir (Roddi, Denis, Tim) uns den Philipp vom cleaneletric Podcast eingeladen um mal ganz ausführlich über den Stand der Dinge in Sachen Elektromobilität in Deutschland und dem Rest der Welt zu sprechen. Dabei versuchen wir zu ergründen, woran es noch hapert und wo vielleicht der Umstieg für viele schon näher liegt, als diese selbst noch glauben.
+ +Dauer: 3:19:11
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Philipp Hellwig + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Die 250. Sendung der Freak Show über die WWDC 2020 und andere Dinge
+ +Nun sind schon über 12 Jahre vergangen seitdem die Freak Show (damals noch unter anderem Namen) ans Netz gegangen ist. Wir bedanken uns für die jahrelange Treue unserer Hörerinnen und Hörer, die mit ihrem Feedback immer dazu beigetragen haben, dass uns die Lust am Projekt nie vergangen ist.
+
+Heute sind wir dann auch standesgemäß vollzählig angetreten und dazu haben wir uns auch noch den Dominik dazugehört, um die Akustik für Euch komplett unübersichtlich zu gestalten.
Dauer: 3:37:40
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Dominik Wagner + | +
+
+
+ |
++ | +
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Die Corona-Situation — Masken und Zauberwürfel — Gesundheitstechnologie — Differential Privacy — macOS auf ARM — Programmiersprachen
+ +Heute begrüßen wir in unserer Runde den Menschen, der jetzt schon seit Jahren auf die eine oder andere Art und Weise zu hören war, nur noch nicht selbst: Rainer, der seinerzeit die Produktion der Previouslys von David übernommen hatte nimmt Platz in unserer Runde. Inhaltlich dreht sich natürlich viel um Das Unvermeidliche Thema ™, aber wir versuchen noch das eine oder andere sinnvolle zur Debatte hinzuzufügen. Und wir mussten mal wieder über Macs reden. Viel Spaß dabei.
+ +Dauer: 4:05:36
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
+
+Rainer
+ Previously + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Die erste Freak Show, die komplett remote stattfindet
+ +Nun ist es es also soweit: der Coronavirus zwingt auch die Freak Show in den Zwangsabstand und so wird das erste mal nicht live und lebendig in der Metaebene aufgenommen, sondern wir finden uns alle digital zusammen. Klappt trotzdem irgendwie und wir bieten seit langem mal wieder ein volles Team. Die Themen liegen natürlich auf der Hand, aber wir reden auch mal wieder über Macs. Viel Spaß.
+ +Dauer: 3:23:01
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Umziehen — Podlove Web Player — Podlove Publisher — WordPress — Podlove Radiator — Küchengeräte
+ +Heute finden wir uns mit einer ganzen Reihe an Wiedergängern aus den letzten Jahren zusammen. Wir begrüßen Dennis Morhardt, Alexander Heimbuch und Eric Teubert in unserer Runde und reden viel über Podlove und Podcasting, aber auch darüber, wie man sich richtig die Hände wäscht und ohne welchen Küchengeräte ein Leben im 21. Jahrhundert unvorstellbar ist.
+ +Dauer: 4:02:47
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Eric Teubert + | +
+
+
+ |
++ | +
|
+
+ |
++Dennis Morhardt + | +
+
+
+ |
++ | +
|
+
+ |
++Alexander Heimbuch + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
+
+Rainer
+ Previously on Freak Show + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Monty Python und Humor — Coronavirus — Living the Future — Raumzeit — Space — Tesla und die Automobilindustrie — Sozialraum Auto — Promotion — Billie Eilish
+ +Eine kleine Runde mit Gast. Tala ist und Gregor ist mal wieder dazugestossen. Wenig überraschend gehen wir diverse Dinge stark auf der Metaebene an (im doppelten Wortsinne). Wir diskutieren Humor, Viren, Texteditoren, das Leben in der Zukunft und warum das mit der Abkehr vom Auto noch eine Weile dauern wird.
+ +Dauer: 4:02:58
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Gregor Sedlag + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Brot backen — Welt putzen — 36C3
+ +Und da sind wir wieder. Wenig überraschend beschäftigen wir uns in dieser Sendung ausführlich mit dem 36. Chaos Communication Congress, haben aber auch noch anderen Unsinn zu bieten. So reden wir über die Adhesivität von Sauerteig und Projekte zum Retten der Welt. Also die richtig wichtigen Dinge. Ach ja und Tala hat jetzt nen Mac. Das Jahrzehnt lässt sich also gut an.
+ +Dauer: 4:14:50
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Camping — Mobilfunktarife — Command Line Tools — Editor Tools — Cybertruck — 10 GBit Ethernet — Apple Pay — German Engineering
+ +Nach längerer Pause begrüßen wir mal wieder Clemens in unserer Runde und auch hukl ist wieder gesund. Wir streiten uns über Mobilfunktarife und das postapokalyptische Design des Cybertrucks. Dazwischen viele Tips für die Freunde der Kommandozeile und des Texteditors.
+ +Dauer: 3:55:26
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
MacBook Pro 16" — AirPods Pro — Tesla — bunq — Laser! — Hong Kong — Pong — Nazi-Krypto — Roundabound
+ +Nach einer längeren Pause sind wir zurück mit einer ganz besonderen Ausgabe: wir begrüßen Laser-Max als Gast in unserer Runde und sprechen ausführlich über alle möglichen Aspekte von Lasertechnologie und dem kreativen Einsatz von Lasern. Außerdem stellen wir fest, dass wir immer schon recht gehabt haben und begrüßen Apple zurück im Reich der Vernünftigen und beklatschen das neueste MacBook Pro und die ENDLICH gefixte Tastatur. Dazu gibt es ein paar kurze Reiseberichte aus Hong Kong und Milton Keynes und natürlich zeigen wir etwas Schadenfreude ob des Tesla-Scoops im Berliner Umland.
+ +Dauer: 4:22:24
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Max Weidling + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Brillen — Rebellion — Banking — Kaffeekocher — Poster Sessions — Hörgeräte — Saugroboter — Events — iOS 13 — macOS Catalina
+ +Tala ist wieder dabei und wir greifen noch einmal das Feedback zum PSD2-Drama auf. Tim hat einige Konten gekündigt und hat sich was neues angeschaut. Ansonsten reden wir viel über die Präsentation von Wissenschaft auf Konferenzen und anderen Kram. Also alles wie immer.
+ +Dauer: 4:13:31
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Segeln — iPad Pro — Das PSD2-Drama — Russen im Apple Store — Podcasts hören — Publishing Software — Scheitern als Chance
+ +In kleiner Runde kommen wir zusammen und hukl und Letty berichten von ihrem jüngsten Segeltörn. Im Anschluss stimmen wir zum großen PSD2-Rant an, bei dem wir mal alles zusammentragen, was in den letzten Wochen und Jahren so an Unbill an deutsche Kontoinhaber zugekommen ist. Tim vertut sich dann noch ein wenig in der Zeit und wir sinnieren über Startups und Failkultur.
+ +Dauer: 3:29:11
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Blinkenlights — Camp Review — Apple Keynote
+ +Pünktlich zur Sendung hat Apple sein neues iPhone-Lineup vorgestellt, so dass wir die Gelegenheit haben, unseren Senf dazuzugeben. Außerdem blicken wir zurück auf Camp und Hafermilch und was uns sonst noch so einfällt. Kleine Runde und kürzere Sendung heute.
+ +Dauer: 2:42:36
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Mit freundlicher Empfehlung von fehlkauf.biz
+ +Wir waren auf dem Camp und haben uns ins der größtmöglichen Teamstärke auf der Bühne des Three Headed Monkey Village live präsentiert. Wirklich alle, die seit Beginn der Freak Show (bzw. mobileMacs) irgendwann zum festen Kader gehörten waren dabei. Entsprechend groß ist natürlich auch das Durcheinander im Gespräch.
+ +Dauer: 1:43:04
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Michaela Lehr + | +
+
+
+ |
++ | +
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
|
+
+ |
++Max von Webel + | +
+
+
+ |
++ | +
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Schwimmen — Packen fürs Camp — Star Wars — Digitale Zahlungssysteme auf Festivals — Deanonymisierung — Nerd-HiFi — Hafermilch
+ +Die Sommerpause läuft aus und wir bieten Euch eine neue Sendung in Kleinstbesetzung. Und da für uns mental das anstehende Chaos Communication Camp dominiert reden wir viel über unseren vor- und diesjährigen Erfahrungen mit dem Open-Air-Dasein mit Nerdappeal. Wir machen kurz einen Ausflug in einer sehr weit entfernte Galaxis vor unserer Zeit und klären dann ultimativ die nahrungskulturelle Bedeutung von Hafermilch. Vielleicht sollten wir mal wieder mehr über Macs reden, aber heute ist es noch nicht so weit.
+ +Dauer: 3:51:31
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Die Nachlese der WWDC 2019
+ +Wenig überraschend setzen wir uns in dieser Folge ausführlich und nachhaltig mit den vielen Neuigkeiten von Apples Worldwide Developers Conference 2019 (WWDC) auseinander. Denn wir wollten schon immer mal wieder mehr über Macs reden und da kommt uns der neue Mac Pro natürlich zu pass. Das Team tritt zudem in übervoller Stärke an: dieses mal sind wir zu sechst, da wir als Gast noch Dominik Wagner (aka monkeydom) in unserer Runde begrüßen dürfen.
+ +Dauer: 5:00:12
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Dominik Wagner + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
+
+Rainer
+ Previously on Freak Show + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Europawahl — Brillen und Sehschwächen — Rødecaster Pro — 68K Prozessor Hacking — Raumzeit — Kinect v4 — Open3D — Mac Pro — USV am Mac — WWDC
+ +Wir sehen schlecht und reden mal darüber. Tim hat den Rødecaster Pro einem ersten Test unterzogen und berichtet. Auch über den Podcast Raumzeit, der jetzt wieder neu anläuft. Tala bringt diverse 3D-Themen auf und Denis berichtet ausführlich von seinen Forschungen an ienem 68010 Prozessor.
+ +Dauer: 4:05:54
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Tala + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Robert Liebo + | ++ | ++ | +
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Vornamen — Sonic Robots — Tastatur und Terminal — AppleScript — Küchengeräte
+ +Heute begrüßen wir wieder einmal einen Gast in unserer Runde: unsere Wahl fiel auf Moritz Simon Geist, Elektroniker und Erzeuger feinster Instrumentalroboter. Wir sprechen über Vorname, elektronische und robotische Klangerzeugung und am Ende ein weiteres Mal über nützliche Roboter in der Küche. Also alles wie immer.
+ +Dauer: 3:56:53
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Moritz Simon Geist + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Europa — Jonathan Pie — Raumfahrt und Kosmologie — Marathonlauf — Apple Card — Skating — Nähen — Küchengeräte — E-Roller — Magic Keyboard — 360 Grad Foto und Video
+ +Nach längerer Pause geht es weiter und wir sprechen über die Erschütterungen in Europa, Fortschritte in der Raumfahrt und ganz viel über Kochen und Küchengeräte. Tim teilt weitere Eindrücke aus Barcelona und unsere Gästin Michaela neue Erkenntnisse zu 360 Grad Fotos und Videos. Für jeden was dabei.
+ +Dauer: 4:15:49
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Michaela Lehr + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
CO2-Kompensation — Tageslängen und Zeitzonen — Demos — Barcelona — Elektroscooter — Pimp My Mac — Macs und ARM — Transkriptionen — Online Banking — USB4
+ +Heute rekrutiert sich das Team ausnahmsweise ohne neuen Gast aus der Stammbesetzung und wir quatschen wie immer daher, wie es uns passt.
+ +Dauer: 4:06:15
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Janis Prefect + | +
+
+
+ |
++ | +
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Heimcomputer — Kunst — Grafiksoftware — Workshops — Kunst und offene Lizenzen — E-Roller Security
+ +Zum dritten Mal hintereinander begrüßen wir einen Gast in unserer Sendung: der Künstler Niklas Roy berichtet von seinem Kunstverständnis und wie er an seine vielfältigen Projekte herangeht, sie dokumentiert und Kunst durch Dokumentation und Workshops zugänglich macht. Außerdem rätseln wir noch ein wenig darüber, warum es wo wie schnell und wie lange hell oder dunkel wird oder bleibt und beweisen unser tiefgreifendes astronomisches Grundverständnis. Gegen Ende berichtet Tim von ersten Erlebnissen und Supermärkten und Shopping Malls in Barcelona und rätseln, wie man mit sprechenden Zylindern Sprachen lernen kann.
+ +Dauer: 3:52:57
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Niklas Roy + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Generative Art — Douglas Adams — Jugend hackt — Lernen durch Erklären — Japan — NFC — Lorawan — Wikidata — SUBSCRIBE 10 — WordPress — Static Website Generators — Ultrawide Monitore — iPhone Xs — Costa Rica
+ +Die letzte Sendung vor Tims Abreise nach Barcelona bringt noch einen neuen Gast in die Runde: wir begrüßen Sabine aka. @bleeptrack und sprechen viel über ihre zahlreichen Hackereien und Erkläereien auf YouTube und ihr Engament bei Jugend hackt und anderen Initiativen und Hackerspaces. Dabei kommen wir gewohnt vom Hölzchen aufs Stöckchen und schaffen wieder unsere vier Stunden. Viel Spaß damit.
+ +Dauer: 4:05:57
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
|
+
+ |
++Bleeptrack + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
+
+Janis Prefect
+ Previously: Ist besser als Schmatzen + |
+
+
+
+ |
++ | +
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Fahradfahren in der Stadt — E-Scooter — E-Roller — Netzbasteln — Sprechendes Denken — Car Sharing — Radio und Podcasts — Mac und Monitor
+ +Die Zusammensetzung des Teams wurde wieder frisch durchgewürfelt: Denis ist wieder mit dabei so wie auch Roddi und als Gast begrüßen wir dieses Mal den Netzbastler und Radioschaffenden Moritz Metz. Wir unterhalten uns intensiv über Konzepte für die Raumordnung des Fahrradverkehrs und die Auswirkung von E-Mobilität. Tim berichtet von seinen neuen Elektrofahrgeräten und dann schauen wir auf Moritz' umfangreiches Werk von fünf Jahren "Netzbasteln", seiner Sendung auf Deutschlandfunk Nova.
+ +Dauer: 4:32:21
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Moritz Metz + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
+
+Rainer
+ Previously: Zelten im Sommerloch + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Barcelona — DeepL — Aufräumen und Ordnung — Chaos und Ordnung — 35C3 — SUBSCRIBE 10 — UKW
+ +In der ersten Sendung des Jahres dreht sich wenig überraschend viel um den 35C3 und wir diskutieren mit unseren zwei Gästen Maria und Gregor ausführlich die Metaebene dieser Veranstaltung. Zuvor sprechen wir noch über europäische Metropolen und über das Wesen des Aufräumens und der Dualität von Chaos und Ordnung.
+ +Dauer: 5:16:10
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Maria Reimer + | +
+
+
+ |
++ | +
|
+
+ |
++Gregor Sedlag + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
+
+Rainer
+ Previously on Freak Show + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
35C3 — Klimawandel und Elektromikromobilität — Geschenke — Gutes tun — Apple Pay — iZettle — DNS — EveryMac — Apple Health — Dual SIM — iPad Pro
+ +Nach einer leider ausgefallenen Sendung kommen wir vor dem Jahresende noch ein letztes Mal zusammen und reden viel Metakram. Außerdem treffen wir uns mit unseren Hörern auf dem 35C3 (am 1. Congresstag am Späti um 20 Uhr) und versprechen, auch im nächsten Jahr weiter zu machen, auch wenn Tim viel in Barcelona sein wird.
+ +Dauer: 4:29:44
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Letty + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
++Sebastian Hauck + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Brexit — E-Scooter — Transistormeter — Community IX — Rødecaster Pro — Twitter — HTTP/3 — TLS Security — Fritz OS 7 und Mesh Networking — Barcelona
+ +Neue Sendung, wieder eine neue Mischung: Clemens ist seit längerem mal wieder mit dabei und überhaupt vermengen mal wieder eine Menge unrelated stuff zu einem Podcast-Potpourri der Unterhaltungsklasse C. Was auch immer das heißen mag. Auf jeden Fall decken wir von Brexit bis DNS über HTTPS alle Abkürzungen ab, die diese Woche relevant sind.
+ +Dauer: 3:50:26
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Clemens Schrimpe + | +
+
+
+ |
++ | +
| +Support + | +|||
|
+
+ |
+
+Sebastian Hauck
+ Previously on Freak Show + |
+
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
Diese Sendung soll nie aufhören?
+Unterstütze die Metaebene mit einer Spende
+
Zypern — Lastenräder und Elektroscooter — MacBook Air — Mac mini — Pimp My Mac — iPad Pro — iPhones und Helium — Spiele — Apple Watch — Apple Pay
+ +Heute wieder eine Sendung in ganz klassischer Besetzung und auch mit klassischer Themenauswahl: wir mussten mal wieder über Macs reden und Anlass dazu gab es in dieser Woche reichlich. Trotzdem hält sich die Zahl der potentiellen Neukäufe stark in Grenzen und wir konzentrieren uns darauf, unsere antiken Großrechner der Vorzeit zu pimpen. Ganz normale Freak Show also.
+ +Dauer: 3:47:06
+ + + +| +On Air + | +|||
|---|---|---|---|
|
+
+ |
++Tim Pritlove + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++roddi + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++hukl + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Denis Ahrens + | +
+
+
+ |
+
+
+
+ |
+
| +Support + | +|||
|
+
+ |
++Rainer + | +
+
+
+ |
+
+
+
+ |
+
|
+
+ |
++Studio Link On Air + | +
+
+
+ |
++ | +
+]]>
We invite you to read our show notes at https://www.grc.com/sn/SN-799-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-799-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-799-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-797-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-797-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-797-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-796-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-796-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-796-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-795-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-795-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-795-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-794-Notes.pdf
+Hosts: Steve Gibson and Jason Howell
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-794-Notes.pdf
+Hosts: Steve Gibson and Jason Howell
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-794-Notes.pdf
+Hosts: Steve Gibson and Jason Howell
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-793-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-793-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-793-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-792-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-792-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-792-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-791-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-791-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-791-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-790-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-790-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>We invite you to read our show notes at https://www.grc.com/sn/SN-790-Notes.pdf
+Hosts: Steve Gibson and Leo Laporte
+Download or subscribe to this show at https://twit.tv/shows/security-now.
+You can submit a question to Security Now! at the GRC Feedback Page.
+For 16kbps versions, transcripts, and notes (including fixes), visit Steve's site: grc.com, also the home of the best disk maintenance and recovery utility ever written Spinrite 6.
+Sponsors:
+]]>