timestamp and unknown episodes

This commit is contained in:
Daniel Langbein 2023-11-09 17:45:25 +01:00
parent 7a9e4babeb
commit 73be70bd4c
Signed by: langfingaz
GPG Key ID: 6C47C753F0823002
4 changed files with 47 additions and 11 deletions

View File

@ -3,6 +3,13 @@
from datetime import datetime, timezone from datetime import datetime, timezone
def test():
utc_timestamp = 1699546201
dt = from_timestamp(utc_timestamp)
assert dt == datetime(2023, 11, 9, 16, 10, 1, tzinfo=timezone.utc)
assert to_timestamp(dt) == utc_timestamp
def now() -> datetime: def now() -> datetime:
return datetime.now(timezone.utc) return datetime.now(timezone.utc)
@ -16,7 +23,7 @@ def now_timestamp() -> int:
def to_timestamp(dt: datetime) -> int: def to_timestamp(dt: datetime) -> int:
return round(datetime.timestamp(dt)) * 1000 return round(datetime.timestamp(dt))
def from_timestamp(timestamp: int) -> datetime: def from_timestamp(timestamp: int) -> datetime:
@ -34,3 +41,7 @@ def from_str(dt_str: str) -> datetime:
def fmt() -> str: def fmt() -> str:
return '%Y-%m-%dT%H:%M:%S' return '%Y-%m-%dT%H:%M:%S'
if __name__ == '__main__':
test()

View File

@ -56,6 +56,13 @@ def cache(file_url: str, cache_dir: Path) -> Path:
""" """
file = hashed_location(file_url, cache_dir) file = hashed_location(file_url, cache_dir)
# Replace plain HTTP with HTTPS
http = 'http://'
https = 'https://'
if file_url.startswith(http):
file_url = https + file_url[len(http):]
assert file_url.startswith(https), file_url
# If the file is missing locally, it is downloaded. # If the file is missing locally, it is downloaded.
if not file.is_file(): if not file.is_file():
Log.info(f'Downloading {file_url} ...') Log.info(f'Downloading {file_url} ...')

View File

@ -106,6 +106,9 @@ class EpisodeAction:
def position(self, value: int): def position(self, value: int):
self._position = value self._position = value
def __str__(self):
return json.dumps(self.to_json())
class NextcloudApi: class NextcloudApi:
@classmethod @classmethod
@ -156,22 +159,21 @@ class NextcloudApi:
return content['add'] return content['add']
@classmethod @classmethod
def fetch_play_actions(cls, server: str, login_name: str, app_password: str, user_id: str): def fetch_play_actions(cls, server: str, login_name: str, app_password: str, user_id: str) -> list[EpisodeAction]:
""" """
:return: Return new episode actions since last function call where EpisodeActionType is PLAY. :return: Return new episode actions since last function call where EpisodeActionType is PLAY.
""" """
timestamp_dict = read_timestamp() timestamp_dict = read_timestamp()
timestamp = timestamp_dict.get(user_id, 0) timestamp = datetime_util.from_str(timestamp_dict.get(user_id, '1970-01-01T00:00:00'))
episode_actions = NextcloudApi.fetch_episode_actions( new_timestamp, episode_actions = NextcloudApi.fetch_episode_actions(
server=server, server=server,
login_name=login_name, login_name=login_name,
app_password=app_password, app_password=app_password,
types=[EpisodeActionType.PLAY], types=[EpisodeActionType.PLAY],
since=timestamp) since=timestamp)
timestamp = datetime_util.now_timestamp() timestamp_dict[user_id] = datetime_util.to_str(new_timestamp)
timestamp_dict[user_id] = timestamp
write_timestamp(timestamp_dict) write_timestamp(timestamp_dict)
return episode_actions return episode_actions
@ -182,24 +184,25 @@ class NextcloudApi:
login_name: str, login_name: str,
app_password: str, app_password: str,
types: list[EpisodeActionType] = None, types: list[EpisodeActionType] = None,
since: int = 0) -> list[EpisodeAction]: since: datetime = datetime_util.from_timestamp(0)) \
-> 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
""" """
if types is None: if types is None:
types = [ea_type for ea_type in EpisodeActionType] types = [ea_type for ea_type in EpisodeActionType]
url = f'{server}/apps/gpoddersync/episode_action?since={since}' url = f'{server}/apps/gpoddersync/episode_action?since={datetime_util.to_timestamp(since)}'
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 assert r.status_code == 200
content = json.loads(r.text) content = json.loads(r.text)
_timestamp = datetime_util.from_timestamp(content['timestamp']) timestamp = datetime_util.from_timestamp(content['timestamp'])
episode_actions = [EpisodeAction.from_json(action) for action in content['actions']] episode_actions = [EpisodeAction.from_json(action) for action in content['actions']]
return [episode_action for episode_action in episode_actions return timestamp, [episode_action for episode_action in episode_actions
if episode_action.action in types] if episode_action.action in types]
@classmethod @classmethod

View File

@ -73,13 +73,25 @@ class User:
episodes_by_id: dict[str, Episode] = {e.episode_id: e episodes_by_id: dict[str, Episode] = {e.episode_id: e
for p in self._podcasts for p in self._podcasts
for e in p.episodes} for e in p.episodes}
# fix broken "guid" of podcast "Forklart" which includes heading and tailing newlines
for episode_action in episode_actions:
episode_action.guid = episode_action.guid.strip()
# fix local files on Android phone
header = 'antennapod_local:content://com.android'
episode_actions = [ea for ea in episode_actions if not ea.podcast.startswith(header)]
for episode_action in episode_actions: for episode_action in episode_actions:
if episode_action.episode_id in episodes_by_id: if episode_action.episode_id in episodes_by_id:
episode = episodes_by_id[episode_action.episode_id] episode = episodes_by_id[episode_action.episode_id]
print(f'\t{episode.title} {episode.get_position()} -> {episode_action.position}')
activity = PlaybackActivity(self.user_id, episode_action.position, episode_action.timestamp) activity = PlaybackActivity(self.user_id, episode_action.position, episode_action.timestamp)
episode.set_activity(activity) episode.set_activity(activity)
episode.set_duration(episode_action.total) episode.set_duration(episode_action.total)
episode.write() episode.write()
else:
# TODO
Log.error(f'Action refers locally unknown episode: {episode_action}')
except requests.exceptions.ConnectionError as _e: except requests.exceptions.ConnectionError as _e:
Log.error(f'Could not fetch new actions.') Log.error(f'Could not fetch new actions.')
@ -266,6 +278,9 @@ class Episode:
def set_activity(self, new_activity: PlaybackActivity): def set_activity(self, new_activity: PlaybackActivity):
prev_activity = self.get_activity() prev_activity = self.get_activity()
if new_activity.timestamp == prev_activity.timestamp and new_activity.position == prev_activity.position:
# This is the current activity, nothing has changed.
return
if new_activity.timestamp <= prev_activity.timestamp: if new_activity.timestamp <= prev_activity.timestamp:
raise ValueError(f'Expected timestamp of new activity to be greater.' raise ValueError(f'Expected timestamp of new activity to be greater.'
f' Previous: {prev_activity.timestamp}.' f' Previous: {prev_activity.timestamp}.'