Skip to content

Commit

Permalink
added docstrings to project
Browse files Browse the repository at this point in the history
  • Loading branch information
mwhickson committed Nov 24, 2024
1 parent 3ce2429 commit d3d141d
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 2 deletions.
20 changes: 20 additions & 0 deletions tuipod/models/episode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
#import playsound3

class Episode:
"""
A minimal representation of a podcast episode.
It also handles playback of the episode (at the moment).
FIX: extract play functionality from model.
"""

def __init__(self, title: str, url: str, description: str, pubdate: str, duration: int) -> None:
"""initialize episode with title, url, description, pubdate and duration"""
self.id = uuid.uuid4().hex
self.title = title
self.url = url
Expand All @@ -22,15 +30,22 @@ def __init__(self, title: str, url: str, description: str, pubdate: str, duratio
self.is_playing = False

def __lt__(self, other):
"""'less than' support to allow episode sorting"""
if self.pubdate != other.pubdate:
return self.pubdate < other.pubdate
else:
return self.title < other.title

def is_playing(self) -> bool:
"""track whether episode is playing or not"""
return self.is_playing()

def play_episode(self):
"""
play the episode audio directly from its internet source
TODO: extract this, and add ability to track playback (and ideally switch from internet play to cached play from disk, once file is downloaded)
"""
if not self.device is None:
self.device.start(self.stream)
else:
Expand All @@ -42,5 +57,10 @@ def play_episode(self):
self.is_playing = True

def stop_episode(self):
"""
stop episode from playing
NOTE: more like pausing, since we don't close/destroy the associated playback device.
"""
self.device.stop()
self.is_playing = False
6 changes: 6 additions & 0 deletions tuipod/models/player.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from tuipod.models.episode import Episode

class Player:
"""
A minimal, abstract player stub for the podcast player.
TODO: flesh this out...
"""

def __init__(self, episode: Episode, position_seconds: int) -> None:
"""initialize a podcast player with the episode to play, and the current position/offset within the episode in seconds"""
self.episode = episode
self.position_seconds = position_seconds
18 changes: 18 additions & 0 deletions tuipod/models/podcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
from tuipod.models.episode import Episode

class Podcast:
"""
A minimal representation of a podcast.
It also handles retrieval of episodes from the podcast feed (at the moment).
FIX: extract episode retrieval into a more appropriate spot.
"""

def __init__(self, title: str, url: str, description: str) -> None:
"""initialize a podcast with title, url, and description"""
self.id = uuid.uuid4().hex
self.title = title
self.url = url
Expand All @@ -16,18 +24,28 @@ def __init__(self, title: str, url: str, description: str) -> None:
self.subscribed = False

def __lt__(self, other):
"""'less than' support to allow podcast sorting"""
return self.title < other.title

def add_episode(self, episode: Episode) -> None:
"""add an episode to the podcast"""
self.episodes.append(episode)

def remove_episode(self, url: str) -> None:
"""remove an episode from the podcast based on the episode URL"""
for e in self.episodes:
if e.url == url:
self.episodes.remove(e)
break

def get_episode_list(self) -> []:
"""
Get an episode list from the podcast feed.
Works around missing data in a tested, but haphazard, manner.
TODO: put this someplace more appropriate.
"""
with urllib.request.urlopen(self.url) as response:
result = response.read()

Expand Down
10 changes: 10 additions & 0 deletions tuipod/models/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
from tuipod.models.podcast import Podcast

class Search:
"""
A barebones search hooked up to the iTunes podcast search service.
TODO: abstract and add additional search providers (e.g. gpodder.net, Spotify, YouTube, etc.)
"""

ENDPOINT = "https://itunes.apple.com/search"

def __init__(self, search_text: str) -> None:
"""initialize search with text to find"""
self.search_text = search_text
self.cached_results = []

def get_cached_search_results(self) -> []:
"""pull search results from cache"""
return self.cached_results

def get_search_results(self) -> []:
"""reach out to provider (iTunes) and retrieve search results"""
results = []

data = {"media": "podcast", "entity": "podcast", "term": self.search_text}
Expand All @@ -41,6 +50,7 @@ def get_search_results(self) -> []:
return results

async def search(self, search_text: str) -> []:
"""based on the search_text supplied, either get fresh results, or pull from cache (if same search is conducted)"""
if self.search_text == search_text:
return self.get_cached_search_results()
else:
Expand Down
8 changes: 8 additions & 0 deletions tuipod/models/subscription_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@
from tuipod.models.podcast import Podcast

class SubscriptionList:
"""
A simple subscription list saved/loaded from a fixed OPML file (subscriptions.opml).
"""

SUBSCRIPTION_FILE = "subscriptions.opml"

def __init__(self) -> None:
"""initialize the subscription list"""
self.podcasts = []

def add_podcast(self, p: Podcast) -> None:
"""add a podcast subscription"""
p.subscribed = True
self.podcasts.append(p)

def remove_podcast(self, url: str) -> None:
"""remove a subscribed podcast by URL"""
for p in self.podcasts:
if p.url == url:
p.subscribed = False
self.podcasts.remove(p)
break

def retrieve(self) -> []:
"""load subscriptions from disk (if the subscription file exists)"""
self.podcasts = []

if exists(self.SUBSCRIPTION_FILE):
Expand All @@ -41,6 +48,7 @@ def retrieve(self) -> []:
self.add_podcast(p)

def persist(self) -> None:
"""save the current subscription list to disk"""
with open(self.SUBSCRIPTION_FILE, "wt", encoding="utf-8") as subscription_file:
lines = []
lines.append('<?xml version="1.0" encoding="utf-8" standalone="no"?>\n')
Expand Down
10 changes: 8 additions & 2 deletions tuipod/ui/about_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@


class AboutInfoScreen(ModalScreen):
"""
An about/help modal screen for tuipod.
"""

BINDINGS = [
("escape", "close_modal", "Close modal")
]
Expand Down Expand Up @@ -77,13 +81,13 @@ class AboutInfoScreen(ModalScreen):
NOTE: Some keystrokes depend on application state (e.g. not actively searching, episode playing, etc.)
"""

# TODO: - `S` - subscribe to the current podcast

def __init__(self) -> None:
"""initialize the about/help screen"""
super().__init__()
self.detail = self.ABOUT_INFO

def compose(self) -> ComposeResult:
"""build the about/help screen"""
yield Container(
Static("About", id="modalTitle"),
Container(
Expand All @@ -98,8 +102,10 @@ def compose(self) -> ComposeResult:
)

def on_button_pressed(self, event: Button.Pressed) -> None:
"""handle button presses (currently just the close button)"""
if event.button.id == "closeInfoButton":
self.app.pop_screen()

def action_close_modal(self) -> None:
"""close the modal"""
self.app.pop_screen()
8 changes: 8 additions & 0 deletions tuipod/ui/episode_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@


class EpisodeInfoScreen(ModalScreen):
"""
An episode information modal screen for tuipod.
"""

BINDINGS = [
("escape", "close_modal", "Close modal")
]
Expand Down Expand Up @@ -61,12 +65,14 @@ class EpisodeInfoScreen(ModalScreen):
"""

def __init__(self, title: str, url: str, detail: str) -> None:
"""initialize the episode information screen"""
super().__init__()
self.title = title
self.url = url
self.detail = detail

def compose(self) -> ComposeResult:
"""build the episode information screen"""
yield Container(
Static("Episode Information", id="modalTitle"),
Container(
Expand All @@ -83,8 +89,10 @@ def compose(self) -> ComposeResult:
)

def on_button_pressed(self, event: Button.Pressed) -> None:
"""handle button presses (currently just the close button)"""
if event.button.id == "closeInfoButton":
self.app.pop_screen()

def action_close_modal(self) -> None:
"""close the modal"""
self.app.pop_screen()
5 changes: 5 additions & 0 deletions tuipod/ui/episode_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from textual.widgets import DataTable

class EpisodeList(Widget):
"""
A simple episode list widget using a datatable for columnar display.
"""

DEFAULT_CSS = """
EpisodeList {
Expand All @@ -11,9 +14,11 @@ class EpisodeList(Widget):
"""

def compose(self) -> ComposeResult:
"""build the widget"""
yield DataTable(id="EpisodeList", cursor_type="row", zebra_stripes=True)

def on_mount(self):
"""set up the columns on mount"""
table: DataTable = self.query_one("#EpisodeList")
table.add_column("Episode Title")
table.add_column("Duration")
Expand Down
8 changes: 8 additions & 0 deletions tuipod/ui/error_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@


class ErrorInfoScreen(ModalScreen):
"""
A generic error display modal screen for tuipod.
"""

BINDINGS = [
("escape", "close_modal", "Close modal")
]
Expand Down Expand Up @@ -51,10 +55,12 @@ class ErrorInfoScreen(ModalScreen):
"""

def __init__(self, detail: str) -> None:
"""initialize the error screen"""
super().__init__()
self.detail = detail

def compose(self) -> ComposeResult:
"""build the error screen"""
yield Container(
Static("Error Information", id="modalTitle"),
Container(
Expand All @@ -69,8 +75,10 @@ def compose(self) -> ComposeResult:
)

def on_button_pressed(self, event: Button.Pressed) -> None:
"""handle button presses (currently just the close button)"""
if event.button.id == "closeInfoButton":
self.app.pop_screen()

def action_close_modal(self) -> None:
"""close the modal"""
self.app.pop_screen()
Loading

0 comments on commit d3d141d

Please sign in to comment.