mygpo: Support for episode actions
Add support for uploading "play", "download", and "delete" actions. Downloading of episode actions is also supported, the downloaded actions are only queued at the moment, not yet processed.
This commit is contained in:
parent
f5b6cebfad
commit
e1173c895a
3 changed files with 88 additions and 30 deletions
|
@ -1919,6 +1919,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
(file_type == 'video' and not self.config.video_played_dbus):
|
||||
# Mark episode as played in the database
|
||||
episode.mark(is_played=True)
|
||||
self.mygpo_client.on_playback([episode])
|
||||
|
||||
filename = episode.local_filename(create=False)
|
||||
if filename is None or not os.path.exists(filename):
|
||||
|
@ -1953,6 +1954,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
log('Executing: %s', repr(command), sender=self)
|
||||
subprocess.Popen(command)
|
||||
|
||||
# Flush updated episode status
|
||||
self.mygpo_client.flush()
|
||||
|
||||
def playback_episodes(self, episodes):
|
||||
episodes = [e for e in episodes if \
|
||||
e.was_downloaded(and_exists=True) or self.streaming_possible()]
|
||||
|
@ -2654,6 +2658,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
episode_urls = set()
|
||||
channel_urls = set()
|
||||
|
||||
episodes_status_update = []
|
||||
for idx, episode in enumerate(episodes):
|
||||
progress.on_progress(float(idx)/float(len(episodes)))
|
||||
if episode.is_locked:
|
||||
|
@ -2664,6 +2669,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
episode.delete_from_disk()
|
||||
episode_urls.add(episode.url)
|
||||
channel_urls.add(episode.channel.url)
|
||||
episodes_status_update.append(episode)
|
||||
|
||||
# Tell the shownotes window that we have removed the episode
|
||||
if self.episode_shownotes_window is not None and \
|
||||
|
@ -2671,6 +2677,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.episode_shownotes_window.episode.url == episode.url:
|
||||
util.idle_add(self.episode_shownotes_window._download_status_changed, None)
|
||||
|
||||
# Notify the web service about the status update + upload
|
||||
self.mygpo_client.on_delete(episodes_status_update)
|
||||
self.mygpo_client.flush()
|
||||
|
||||
util.idle_add(finish_deletion, episode_urls, channel_urls)
|
||||
|
||||
threading.Thread(target=thread_proc).start()
|
||||
|
@ -2812,11 +2822,15 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if add_paused:
|
||||
task.status = task.PAUSED
|
||||
else:
|
||||
self.mygpo_client.on_download([task.episode])
|
||||
self.download_queue_manager.add_task(task)
|
||||
|
||||
self.download_status_model.register_task(task)
|
||||
self.enable_download_list_update()
|
||||
|
||||
# Flush updated episode status
|
||||
self.mygpo_client.flush()
|
||||
|
||||
def cancel_task_list(self, tasks):
|
||||
if not tasks:
|
||||
return
|
||||
|
|
|
@ -41,7 +41,11 @@ class Store(object):
|
|||
return class_.__name__, list(sorted(class_.__slots__))
|
||||
|
||||
def _set(self, o, slot, value):
|
||||
setattr(o, slot, o.__class__.__slots__[slot](value))
|
||||
# Set a slot on the given object to value, doing a cast if
|
||||
# necessary. The value None is special-cased and never cast.
|
||||
if value is not None:
|
||||
value = o.__class__.__slots__[slot](value)
|
||||
setattr(o, slot, value)
|
||||
|
||||
def commit(self):
|
||||
with self.lock:
|
||||
|
@ -82,6 +86,10 @@ class Store(object):
|
|||
with self.lock:
|
||||
self._register(o.__class__)
|
||||
table, slots = self._schema(o.__class__)
|
||||
|
||||
# Only save values that have values set (non-None values)
|
||||
slots = [s for s in slots if getattr(o, s) is not None]
|
||||
|
||||
values = [str(getattr(o, slot)) for slot in slots]
|
||||
self.db.execute('INSERT INTO %s (%s) VALUES (%s)' % (table,
|
||||
', '.join(slots), ', '.join('?'*len(slots))), values)
|
||||
|
@ -95,6 +103,10 @@ class Store(object):
|
|||
with self.lock:
|
||||
self._register(o.__class__)
|
||||
table, slots = self._schema(o.__class__)
|
||||
|
||||
# Use "None" as wildcard selector in remove actions
|
||||
slots = [s for s in slots if getattr(o, s, None) is not None]
|
||||
|
||||
values = [getattr(o, slot) for slot in slots]
|
||||
self.db.execute('DELETE FROM %s WHERE %s' % (table,
|
||||
' AND '.join('%s=?'%s for s in slots)), values)
|
||||
|
|
|
@ -28,6 +28,7 @@ import gpodder
|
|||
_ = gpodder.gettext
|
||||
|
||||
import atexit
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
@ -43,6 +44,7 @@ mygpoclient.user_agent += ' ' + gpodder.user_agent
|
|||
|
||||
from mygpoclient import api
|
||||
|
||||
from mygpoclient import util as mygpoutil
|
||||
|
||||
|
||||
# Database model classes
|
||||
|
@ -213,17 +215,42 @@ class MygPoClient(object):
|
|||
# After we've handled the reverse-actions, clean up
|
||||
self._store.remove(actions)
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._config.mygpo_server
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
return self._config.mygpo_device_uid
|
||||
|
||||
def can_access_webservice(self):
|
||||
return self._config.mygpo_enabled and self._config.mygpo_device_uid
|
||||
|
||||
def set_subscriptions(self, urls):
|
||||
if self.can_access_webservice():
|
||||
log('Uploading (overwriting) subscriptions...')
|
||||
self._client.put_subscriptions(self._config.mygpo_device_uid, urls)
|
||||
self._client.put_subscriptions(self.device_id, urls)
|
||||
log('Subscription upload done.')
|
||||
else:
|
||||
raise Exception('Webservice access not enabled')
|
||||
|
||||
def _convert_episode(self, episode, action):
|
||||
return EpisodeAction(episode.channel.url, \
|
||||
episode.url, self.device_id, action, \
|
||||
int(time.time()), None)
|
||||
|
||||
def on_delete(self, episodes):
|
||||
log('Storing %d episode delete actions', len(episodes), sender=self)
|
||||
self._store.save(self._convert_episode(e, 'delete') for e in episodes)
|
||||
|
||||
def on_download(self, episodes):
|
||||
log('Storing %d episode download actions', len(episodes), sender=self)
|
||||
self._store.save(self._convert_episode(e, 'download') for e in episodes)
|
||||
|
||||
def on_playback(self, episodes):
|
||||
log('Storing %d episode playback actions', len(episodes), sender=self)
|
||||
self._store.save(self._convert_episode(e, 'play') for e in episodes)
|
||||
|
||||
def on_subscribe(self, urls):
|
||||
# Cancel previously-inserted "remove" actions
|
||||
self._store.remove(SubscribeAction.remove(url) for url in urls)
|
||||
|
@ -330,51 +357,56 @@ class MygPoClient(object):
|
|||
self._store.remove(self._store.load(UpdateDeviceAction))
|
||||
|
||||
# Insert our new update action
|
||||
action = UpdateDeviceAction(self._config.mygpo_device_uid, \
|
||||
action = UpdateDeviceAction(self.device_id, \
|
||||
self._config.mygpo_device_caption, \
|
||||
self._config.mygpo_device_type)
|
||||
self._store.save(action)
|
||||
|
||||
def synchronize_episodes(self, actions):
|
||||
log('Info: Episode sync disabled at the moment.', sender=self)
|
||||
return True
|
||||
|
||||
log('Starting episode status sync.', sender=self)
|
||||
|
||||
def convert_to_api(action):
|
||||
dt = datetime.datetime.fromtimestamp(action.timestamp)
|
||||
since = mygpoutil.datetime_to_iso8601(dt)
|
||||
return api.EpisodeAction(action.podcast_url, \
|
||||
action.episode_url, action.action, \
|
||||
action.device_id, action.timestamp, \
|
||||
action.device_id, since, \
|
||||
action.position)
|
||||
|
||||
def convert_from_api(action):
|
||||
dt = mygpoutil.iso8601_to_datetime(action.timestamp)
|
||||
since = int(dt.strftime('%s'))
|
||||
return ReceivedEpisodeAction(action.podcast, \
|
||||
action.episode, action.device, \
|
||||
action.action, action.timestamp, \
|
||||
action.position)
|
||||
action.action, since, action.position)
|
||||
|
||||
try:
|
||||
host = self._config.mygpo_server
|
||||
device_id = self._config.mygpo_device_uid
|
||||
save_since = True
|
||||
|
||||
# Load the "since" value from the database
|
||||
since_o = self._store.get(SinceValue, host=host, \
|
||||
device_id=device_id, \
|
||||
since_o = self._store.get(SinceValue, host=self.host, \
|
||||
device_id=self.device_id, \
|
||||
category=SinceValue.EPISODES)
|
||||
|
||||
# Use a default since object for the first-time case
|
||||
if since_o is None:
|
||||
since_o = SinceValue(host, device_id, SinceValue.EPISODES)
|
||||
since_o = SinceValue(self.host, self.device_id, SinceValue.EPISODES)
|
||||
|
||||
# Step 1: Download Episode actions
|
||||
try:
|
||||
changes = self._client.download_episode_actions(since_o.since, \
|
||||
device_id=device_id)
|
||||
device_id=self.device_id)
|
||||
|
||||
received_actions = [convert_from_api(a) for a in changes.actions]
|
||||
self._store.save(received_actions)
|
||||
|
||||
# Save the "since" value for later use
|
||||
self._store.update(since_o, since=changes.since)
|
||||
except Exception, e:
|
||||
log('Exception while polling for episodes.', sender=self, traceback=True)
|
||||
save_since = False
|
||||
|
||||
# Step 2: Upload Episode actions
|
||||
|
||||
# Convert actions to the mygpoclient format for uploading
|
||||
episode_actions = [convert_to_api(a) for a in actions]
|
||||
|
@ -382,8 +414,9 @@ class MygPoClient(object):
|
|||
# Upload the episodes and retrieve the new "since" value
|
||||
since = self._client.upload_episode_actions(episode_actions)
|
||||
|
||||
if save_since:
|
||||
# Update the "since" value of the episodes
|
||||
self._store.update(since_o, since)
|
||||
self._store.update(since_o, since=since)
|
||||
|
||||
# Actions have been uploaded to the server - remove them
|
||||
self._store.remove(actions)
|
||||
|
@ -396,20 +429,17 @@ class MygPoClient(object):
|
|||
def synchronize_subscriptions(self, actions):
|
||||
log('Starting subscription sync.', sender=self)
|
||||
try:
|
||||
host = self._config.mygpo_server
|
||||
device_id = self._config.mygpo_device_uid
|
||||
|
||||
# Load the "since" value from the database
|
||||
since_o = self._store.get(SinceValue, host=host, \
|
||||
device_id=device_id, \
|
||||
since_o = self._store.get(SinceValue, host=self.host, \
|
||||
device_id=self.device_id, \
|
||||
category=SinceValue.PODCASTS)
|
||||
|
||||
# Use a default since object for the first-time case
|
||||
if since_o is None:
|
||||
since_o = SinceValue(host, device_id, SinceValue.PODCASTS)
|
||||
since_o = SinceValue(self.host, self.device_id, SinceValue.PODCASTS)
|
||||
|
||||
# Step 1: Pull updates from the server and notify the frontend
|
||||
result = self._client.pull_subscriptions(device_id, since_o.since)
|
||||
result = self._client.pull_subscriptions(self.device_id, since_o.since)
|
||||
|
||||
# Update the "since" value in the database
|
||||
self._store.update(since_o, since=result.since)
|
||||
|
@ -419,10 +449,12 @@ class MygPoClient(object):
|
|||
for url in result.add:
|
||||
log('Received add action: %s', url, sender=self)
|
||||
self._store.remove(ReceivedSubscribeAction.remove(url))
|
||||
self._store.remove(ReceivedSubscribeAction.add(url))
|
||||
self._store.save(ReceivedSubscribeAction.add(url))
|
||||
for url in result.remove:
|
||||
log('Received remove action: %s', url, sender=self)
|
||||
self._store.remove(ReceivedSubscribeAction.add(url))
|
||||
self._store.remove(ReceivedSubscribeAction.remove(url))
|
||||
self._store.save(ReceivedSubscribeAction.remove(url))
|
||||
|
||||
# Step 2: Push updates to the server and rewrite URLs (if any)
|
||||
|
@ -434,7 +466,7 @@ class MygPoClient(object):
|
|||
if add or remove:
|
||||
log('Uploading: +%d / -%d', len(add), len(remove), sender=self)
|
||||
# Only do a push request if something has changed
|
||||
result = self._client.update_subscriptions(device_id, add, remove)
|
||||
result = self._client.update_subscriptions(self.device_id, add, remove)
|
||||
|
||||
# Update the "since" value in the database
|
||||
self._store.update(since_o, since=result.since)
|
||||
|
|
Loading…
Reference in a new issue