diff --git a/Alpine/APKGBUILD b/Alpine/APKGBUILD index 53435c1..a3e474e 100644 --- a/Alpine/APKGBUILD +++ b/Alpine/APKGBUILD @@ -10,7 +10,7 @@ # .apk file extension == .tar.gz file extension pkgname=py3-nextcast -pkgver=0.0.3 +pkgver=0.0.4 pkgrel=1 pkgdesc="Nextcloud Podcast Client" url="https://git.privacy1st.de/langfingaz/nextcast" diff --git a/README.md b/README.md index b160916..114afde 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ cp Alpine/APKGBUILD ~/.local/var/pmbootstrap/cache_git/pmaports/main/py3-nextcas pmbootstrap apkbuild_parse py3-nextcast pmbootstrap checksum py3-nextcast pmbootstrap build --arch aarch64 py3-nextcast -#=> build x86_64/py3-nextcast-0.0.3-r1.apk +#=> build x86_64/py3-nextcast-0.0.4-r1.apk ``` ```shell @@ -63,8 +63,8 @@ pmbootstrap shutdown ``` ```shell -ls ~/.local/var/pmbootstrap/packages/edge/x86_64/py3-nextcast-0.0.3-r1.apk -ls ~/.local/var/pmbootstrap/packages/edge/x86_64/py3-nextcast-pyc-0.0.3-r1.apk +ls ~/.local/var/pmbootstrap/packages/edge/x86_64/py3-nextcast-0.0.4-r1.apk +ls ~/.local/var/pmbootstrap/packages/edge/x86_64/py3-nextcast-pyc-0.0.4-r1.apk ``` Sideload to your postmarketOS phone: @@ -74,3 +74,8 @@ Sideload to your postmarketOS phone: ```shell pmbootstrap sideload --host yodaEnchilada --user yoda --arch aarch64 --install-key py3-nextcast ``` + +## TODOs + +- [ ] If action refers unknown episode/podcast, create it. +- [ ] Executable to update local list of podcasts and episodes. diff --git a/setup.cfg b/setup.cfg index 076df22..26561dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ [metadata] name = nextcast -version = 0.0.3 +version = 0.0.4 author = Daniel Langbein author_email = daniel@systemli.org description = Nextcloud Podcast Client diff --git a/src/nextcast/app_play_episode.py b/src/nextcast/app_play_episode.py index c14d7bb..e9a5752 100755 --- a/src/nextcast/app_play_episode.py +++ b/src/nextcast/app_play_episode.py @@ -7,6 +7,7 @@ from typing import Callable from nextcast import mpv from nextcast.cli_gui import list_selection, CancelButton, TopButton from nextcast.log import Log +from nextcast.nextcloud import LoginExpired from nextcast.podcast import User, Episode, Podcast from nextcast.user_manager import UserManager @@ -36,6 +37,10 @@ def podcast_loop(prev: Callable, user: User) -> None: title='Select podcast', top_buttons=[back_str], ) + except LoginExpired as _e: + Log.error(f'Nextcloud login of user {user.user_id} expired. Please re-login.') + UserManager.logout(user) + exit() except CancelButton as _e: exit() except TopButton as e: diff --git a/src/nextcast/app_resume_playback.py b/src/nextcast/app_resume_playback.py index 2af3389..3ab6130 100755 --- a/src/nextcast/app_resume_playback.py +++ b/src/nextcast/app_resume_playback.py @@ -3,6 +3,7 @@ from nextcast import mpv from nextcast.cli_gui import list_selection, CancelButton from nextcast.log import Log +from nextcast.nextcloud import LoginExpired from nextcast.user_manager import UserManager @@ -12,7 +13,12 @@ def main(): Log.config(pause_on_error=True) user = UserManager.from_interaction() - episodes = user.get_episodes() + try: + episodes = user.get_episodes() + except LoginExpired as _e: + Log.error(f'Nextcloud login of user {user.user_id} expired. Please re-login.') + UserManager.logout(user) + exit() # Paused episodes. pauseds = [episode for episode in episodes diff --git a/src/nextcast/nextcloud.py b/src/nextcast/nextcloud.py index b7defe5..ff38602 100644 --- a/src/nextcast/nextcloud.py +++ b/src/nextcast/nextcloud.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import json +import socket import time import webbrowser from datetime import datetime, timedelta @@ -110,6 +111,10 @@ class EpisodeAction: return json.dumps(self.to_json()) +class LoginExpired(Exception): + status_code = 401 + + class NextcloudApi: @classmethod def login(cls, domain: str) -> tuple[str, str, str]: @@ -117,7 +122,11 @@ class NextcloudApi: Login flow documentation: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html """ - r = requests.post(f'https://{domain}/index.php/login/v2') + # Set human-readable user agent when logging in. + # This string is displayed in the Nextcloud web interface under "Device & Sessions". + hostname: str = socket.gethostname() + headers = {'User-Agent': f'nextcast@{hostname}'} + r = requests.post(f'https://{domain}/index.php/login/v2', headers=headers) assert r.status_code == 200 content = json.loads(r.text) webbrowser.open(content['login'], new=0, autoraise=True) @@ -149,12 +158,16 @@ class NextcloudApi: https://github.com/thrillfall/nextcloud-gpodder/blob/main/README.md#subscription :return: The URLs of the subscribed podcast feeds. Example: 'https://logbuch-netzpolitik.de/feed/opus' + :raises LoginExpired: """ url = f'{server}/apps/gpoddersync/subscriptions' Log.info(f'Downloading {url} ...') # auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication r = requests.get(url, auth=(login_name, app_password)) - assert r.status_code == 200 + if r.status_code == LoginExpired.status_code: + raise LoginExpired() + else: + assert r.status_code == 200, r.status_code content = json.loads(r.text) return content['add'] @@ -188,6 +201,8 @@ class NextcloudApi: -> tuple[datetime, list[EpisodeAction]]: """ https://github.com/thrillfall/nextcloud-gpodder/blob/main/README.md#episode-action + + :raises LoginExpired: """ if types is None: types = [ea_type for ea_type in EpisodeActionType] @@ -196,7 +211,10 @@ class NextcloudApi: Log.info(f'Downloading {url} ...') # auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication r = requests.get(url, auth=(login_name, app_password)) - assert r.status_code == 200 + if r.status_code == LoginExpired.status_code: + raise LoginExpired() + else: + assert r.status_code == 200, r.status_code content = json.loads(r.text) timestamp = datetime_util.from_timestamp(content['timestamp']) @@ -208,6 +226,9 @@ class NextcloudApi: @classmethod def push_episode_actions(cls, server: str, login_name: str, app_password: str, actions: list[EpisodeAction]) -> None: + """ + :raises LoginExpired: + """ data = [action.to_json() for action in actions] url = f'{server}/apps/gpoddersync/episode_action/create' @@ -216,7 +237,10 @@ class NextcloudApi: headers = {'Content-type': 'application/json'} r = requests.post(url, auth=(login_name, app_password), headers=headers, data=data_str) - assert r.status_code == 200 + if r.status_code == LoginExpired.status_code: + raise LoginExpired() + else: + assert r.status_code == 200, r.status_code if __name__ == '__main__': diff --git a/src/nextcast/user_manager.py b/src/nextcast/user_manager.py index 015cebf..fee4423 100644 --- a/src/nextcast/user_manager.py +++ b/src/nextcast/user_manager.py @@ -35,6 +35,11 @@ class UserManager: cls._get_content()[user.user_id] = user.app_password json_util.write_credentials(cls._get_content()) + @classmethod + def remove_from_disk(cls, user: User) -> None: + cls._get_content().pop(user.user_id, None) + json_util.write_credentials(cls._get_content()) + @classmethod def from_interaction(cls) -> User: """ @@ -84,3 +89,7 @@ class UserManager: cls.add_to_disk(user) return user + + @classmethod + def logout(cls, user: User): + cls.remove_from_disk(user)