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
pkgname=py3-nextcast
pkgver=0.0.3
pkgver=0.0.4
pkgrel=1
pkgdesc="Nextcloud Podcast Client"
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 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.

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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__':

View File

@ -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)