user-agent, expired login, TODOs
This commit is contained in:
parent
b06233daa8
commit
dc598223a6
@ -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"
|
||||||
|
11
README.md
11
README.md
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
try:
|
||||||
episodes = user.get_episodes()
|
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
|
||||||
|
@ -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__':
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user