user-agent, expired login, TODOs

This commit is contained in:
Daniel Langbein 2023-11-13 12:26:01 +01:00
parent b06233daa8
commit dc598223a6
Signed by: langfingaz
GPG Key ID: 6C47C753F0823002
7 changed files with 59 additions and 10 deletions

View File

@ -10,7 +10,7 @@
# .apk file extension == .tar.gz file extension # .apk file extension == .tar.gz file extension
pkgname=py3-nextcast pkgname=py3-nextcast
pkgver=0.0.3 pkgver=0.0.4
pkgrel=1 pkgrel=1
pkgdesc="Nextcloud Podcast Client" pkgdesc="Nextcloud Podcast Client"
url="https://git.privacy1st.de/langfingaz/nextcast" url="https://git.privacy1st.de/langfingaz/nextcast"

View File

@ -55,7 +55,7 @@ cp Alpine/APKGBUILD ~/.local/var/pmbootstrap/cache_git/pmaports/main/py3-nextcas
pmbootstrap apkbuild_parse py3-nextcast pmbootstrap apkbuild_parse py3-nextcast
pmbootstrap checksum py3-nextcast pmbootstrap checksum py3-nextcast
pmbootstrap build --arch aarch64 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 ```shell
@ -63,8 +63,8 @@ pmbootstrap shutdown
``` ```
```shell ```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-0.0.4-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-pyc-0.0.4-r1.apk
``` ```
Sideload to your postmarketOS phone: Sideload to your postmarketOS phone:
@ -74,3 +74,8 @@ Sideload to your postmarketOS phone:
```shell ```shell
pmbootstrap sideload --host yodaEnchilada --user yoda --arch aarch64 --install-key py3-nextcast 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.

View File

@ -3,7 +3,7 @@
[metadata] [metadata]
name = nextcast name = nextcast
version = 0.0.3 version = 0.0.4
author = Daniel Langbein author = Daniel Langbein
author_email = daniel@systemli.org author_email = daniel@systemli.org
description = Nextcloud Podcast Client description = Nextcloud Podcast Client

View File

@ -7,6 +7,7 @@ from typing import Callable
from nextcast import mpv from nextcast import mpv
from nextcast.cli_gui import list_selection, CancelButton, TopButton from nextcast.cli_gui import list_selection, CancelButton, TopButton
from nextcast.log import Log from nextcast.log import Log
from nextcast.nextcloud import LoginExpired
from nextcast.podcast import User, Episode, Podcast from nextcast.podcast import User, Episode, Podcast
from nextcast.user_manager import UserManager from nextcast.user_manager import UserManager
@ -36,6 +37,10 @@ def podcast_loop(prev: Callable, user: User) -> None:
title='Select podcast', title='Select podcast',
top_buttons=[back_str], 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: except CancelButton as _e:
exit() exit()
except TopButton as e: except TopButton as e:

View File

@ -3,6 +3,7 @@
from nextcast import mpv from nextcast import mpv
from nextcast.cli_gui import list_selection, CancelButton from nextcast.cli_gui import list_selection, CancelButton
from nextcast.log import Log from nextcast.log import Log
from nextcast.nextcloud import LoginExpired
from nextcast.user_manager import UserManager from nextcast.user_manager import UserManager
@ -12,7 +13,12 @@ def main():
Log.config(pause_on_error=True) Log.config(pause_on_error=True)
user = UserManager.from_interaction() 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. # Paused episodes.
pauseds = [episode for episode in episodes pauseds = [episode for episode in episodes

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import socket
import time import time
import webbrowser import webbrowser
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -110,6 +111,10 @@ class EpisodeAction:
return json.dumps(self.to_json()) return json.dumps(self.to_json())
class LoginExpired(Exception):
status_code = 401
class NextcloudApi: class NextcloudApi:
@classmethod @classmethod
def login(cls, domain: str) -> tuple[str, str, str]: def login(cls, domain: str) -> tuple[str, str, str]:
@ -117,7 +122,11 @@ class NextcloudApi:
Login flow documentation: Login flow documentation:
https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html 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 assert r.status_code == 200
content = json.loads(r.text) content = json.loads(r.text)
webbrowser.open(content['login'], new=0, autoraise=True) 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 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' :return: The URLs of the subscribed podcast feeds. Example: 'https://logbuch-netzpolitik.de/feed/opus'
:raises LoginExpired:
""" """
url = f'{server}/apps/gpoddersync/subscriptions' url = f'{server}/apps/gpoddersync/subscriptions'
Log.info(f'Downloading {url} ...') Log.info(f'Downloading {url} ...')
# auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication # auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication
r = requests.get(url, auth=(login_name, app_password)) 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) content = json.loads(r.text)
return content['add'] return content['add']
@ -188,6 +201,8 @@ class NextcloudApi:
-> tuple[datetime, list[EpisodeAction]]: -> tuple[datetime, list[EpisodeAction]]:
""" """
https://github.com/thrillfall/nextcloud-gpodder/blob/main/README.md#episode-action https://github.com/thrillfall/nextcloud-gpodder/blob/main/README.md#episode-action
:raises LoginExpired:
""" """
if types is None: if types is None:
types = [ea_type for ea_type in EpisodeActionType] types = [ea_type for ea_type in EpisodeActionType]
@ -196,7 +211,10 @@ class NextcloudApi:
Log.info(f'Downloading {url} ...') Log.info(f'Downloading {url} ...')
# auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication # auth parameter: https://requests.readthedocs.io/en/latest/user/authentication/#basic-authentication
r = requests.get(url, auth=(login_name, app_password)) 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) content = json.loads(r.text)
timestamp = datetime_util.from_timestamp(content['timestamp']) timestamp = datetime_util.from_timestamp(content['timestamp'])
@ -208,6 +226,9 @@ class NextcloudApi:
@classmethod @classmethod
def push_episode_actions(cls, server: str, login_name: str, app_password: str, def push_episode_actions(cls, server: str, login_name: str, app_password: str,
actions: list[EpisodeAction]) -> None: actions: list[EpisodeAction]) -> None:
"""
:raises LoginExpired:
"""
data = [action.to_json() for action in actions] data = [action.to_json() for action in actions]
url = f'{server}/apps/gpoddersync/episode_action/create' url = f'{server}/apps/gpoddersync/episode_action/create'
@ -216,7 +237,10 @@ class NextcloudApi:
headers = {'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}
r = requests.post(url, auth=(login_name, app_password), r = requests.post(url, auth=(login_name, app_password),
headers=headers, data=data_str) 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__': if __name__ == '__main__':

View File

@ -35,6 +35,11 @@ class UserManager:
cls._get_content()[user.user_id] = user.app_password cls._get_content()[user.user_id] = user.app_password
json_util.write_credentials(cls._get_content()) 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 @classmethod
def from_interaction(cls) -> User: def from_interaction(cls) -> User:
""" """
@ -84,3 +89,7 @@ class UserManager:
cls.add_to_disk(user) cls.add_to_disk(user)
return user return user
@classmethod
def logout(cls, user: User):
cls.remove_from_disk(user)