home *** CD-ROM | disk | FTP | other *** search
/ Freelog 125 / Freelog_MarsAvril2015_No125.iso / Internet / gpodder / gpodder-portable.exe / gpodder-portable / src / mygpoclient / api.py < prev    next >
Text File  |  2014-10-30  |  16KB  |  424 lines

  1. # -*- coding: utf-8 -*-
  2. # gpodder.net API Client
  3. # Copyright (C) 2009-2013 Thomas Perl and the gPodder Team
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  17.  
  18. import mygpoclient
  19.  
  20. from mygpoclient import util
  21. from mygpoclient import simple
  22. from mygpoclient import public
  23.  
  24. # Additional error types for the advanced API client
  25. class InvalidResponse(Exception): pass
  26.  
  27.  
  28. class UpdateResult(object):
  29.     """Container for subscription update results
  30.  
  31.     Attributes:
  32.     update_urls - A list of (old_url, new_url) tuples
  33.     since - A timestamp value for use in future requests
  34.     """
  35.     def __init__(self, update_urls, since):
  36.         self.update_urls = update_urls
  37.         self.since = since
  38.  
  39. class SubscriptionChanges(object):
  40.     """Container for subscription changes
  41.  
  42.     Attributes:
  43.     add - A list of URLs that have been added
  44.     remove - A list of URLs that have been removed
  45.     since - A timestamp value for use in future requests
  46.     """
  47.     def __init__(self, add, remove, since):
  48.         self.add = add
  49.         self.remove = remove
  50.         self.since = since
  51.  
  52. class EpisodeActionChanges(object):
  53.     """Container for added episode actions
  54.  
  55.     Attributes:
  56.     actions - A list of EpisodeAction objects
  57.     since - A timestamp value for use in future requests
  58.     """
  59.     def __init__(self, actions, since):
  60.         self.actions = actions
  61.         self.since = since
  62.  
  63. class PodcastDevice(object):
  64.     """This class encapsulates a podcast device
  65.  
  66.     Attributes:
  67.     device_id - The ID used to refer to this device
  68.     caption - A user-defined "name" for this device
  69.     type - A valid type of podcast device (see VALID_TYPES)
  70.     subscriptions - The number of podcasts this device is subscribed to
  71.     """
  72.     VALID_TYPES = ('desktop', 'laptop', 'mobile', 'server', 'other')
  73.  
  74.     def __init__(self, device_id, caption, type, subscriptions):
  75.         # Check if the device type is valid
  76.         if type not in self.VALID_TYPES:
  77.             raise ValueError('Invalid device type "%s" (see VALID_TYPES)' % type)
  78.  
  79.         # Check if subsciptions is a numeric value
  80.         try:
  81.             int(subscriptions)
  82.         except:
  83.             raise ValueError('Subscription must be a numeric value but was %s' % subscriptions)
  84.  
  85.         self.device_id = device_id
  86.         self.caption = caption
  87.         self.type = type
  88.         self.subscriptions = int(subscriptions)
  89.  
  90.     def __str__(self):
  91.         """String representation of this device
  92.  
  93.         >>> device = PodcastDevice('mygpo', 'My Device', 'mobile', 10)
  94.         >>> print device
  95.         PodcastDevice('mygpo', 'My Device', 'mobile', 10)
  96.         """
  97.         return '%s(%r, %r, %r, %r)' % (self.__class__.__name__,
  98.                 self.device_id, self.caption, self.type, self.subscriptions)
  99.  
  100.     @classmethod
  101.     def from_dictionary(cls, d):
  102.         return cls(d['id'], d['caption'], d['type'], d['subscriptions'])
  103.  
  104. class EpisodeAction(object):
  105.     """This class encapsulates an episode action
  106.  
  107.     The mandatory attributes are:
  108.     podcast - The feed URL of the podcast
  109.     episode - The enclosure URL or GUID of the episode
  110.     action - One of 'download', 'play', 'delete' or 'new'
  111.  
  112.     The optional attributes are:
  113.     device - The device_id on which the action has taken place
  114.     timestamp - When the action took place (in XML time format)
  115.     started - The start time of a play event in seconds
  116.     position - The current position of a play event in seconds
  117.     total - The total time of the episode (for play events)
  118.  
  119.     The attribute "position" is only valid for "play" action types.
  120.     """
  121.     VALID_ACTIONS = ('download', 'play', 'delete', 'new', 'flattr')
  122.  
  123.     def __init__(self, podcast, episode, action,
  124.             device=None, timestamp=None,
  125.             started=None, position=None, total=None):
  126.         # Check if the action is valid
  127.         if action not in self.VALID_ACTIONS:
  128.             raise ValueError('Invalid action type "%s" (see VALID_ACTIONS)' % action)
  129.  
  130.         # Disallow play-only attributes for non-play actions
  131.         if action != 'play':
  132.             if started is not None:
  133.                 raise ValueError('Started can only be set for the "play" action')
  134.             elif position is not None:
  135.                 raise ValueError('Position can only be set for the "play" action')
  136.             elif total is not None:
  137.                 raise ValueError('Total can only be set for the "play" action')
  138.  
  139.         # Check the format of the timestamp value
  140.         if timestamp is not None:
  141.             if util.iso8601_to_datetime(timestamp) is None:
  142.                 raise ValueError('Timestamp has to be in ISO 8601 format but was %s' % timestamp)
  143.  
  144.         # Check if we have a "position" value if we have started or total
  145.         if position is None and (started is not None or total is not None):
  146.             raise ValueError('Started or total set, but no position given')
  147.  
  148.         # Check that "started" is a number if it's set
  149.         if started is not None:
  150.             try:
  151.                 started = int(started)
  152.             except ValueError:
  153.                 raise ValueError('Started must be an integer value (seconds) but was %s' % started)
  154.  
  155.         # Check that "position" is a number if it's set
  156.         if position is not None:
  157.             try:
  158.                 position = int(position)
  159.             except ValueError:
  160.                 raise ValueError('Position must be an integer value (seconds) but was %s' % position)
  161.  
  162.         # Check that "total" is a number if it's set
  163.         if total is not None:
  164.             try:
  165.                 total = int(total)
  166.             except ValueError:
  167.                 raise ValueError('Total must be an integer value (seconds) but was %s' % total)
  168.  
  169.         self.podcast = podcast
  170.         self.episode = episode
  171.         self.action = action
  172.         self.device = device
  173.         self.timestamp = timestamp
  174.         self.started = started
  175.         self.position = position
  176.         self.total = total
  177.  
  178.     @classmethod
  179.     def from_dictionary(cls, d):
  180.         return cls(d['podcast'], d['episode'], d['action'],
  181.                    d.get('device'), d.get('timestamp'),
  182.                    d.get('started'), d.get('position'), d.get('total'))
  183.  
  184.     def to_dictionary(self):
  185.         d = {}
  186.  
  187.         for mandatory in ('podcast', 'episode', 'action'):
  188.             value = getattr(self, mandatory)
  189.             d[mandatory] = value
  190.  
  191.         for optional in ('device', 'timestamp',
  192.                 'started', 'position', 'total'):
  193.             value = getattr(self, optional)
  194.             if value is not None:
  195.                 d[optional] = value
  196.  
  197.         return d
  198.  
  199. class MygPodderClient(simple.SimpleClient):
  200.     """gpodder.net API Client
  201.  
  202.     This is the API client that implements both the Simple and
  203.     Advanced API of gpodder.net. See the SimpleClient class
  204.     for a smaller class that only implements the Simple API.
  205.     """
  206.  
  207.     @simple.needs_credentials
  208.     def get_subscriptions(self, device):
  209.         # Overloaded to accept PodcastDevice objects as arguments
  210.         device = getattr(device, 'device_id', device)
  211.         return simple.SimpleClient.get_subscriptions(self, device)
  212.  
  213.     @simple.needs_credentials
  214.     def put_subscriptions(self, device, urls):
  215.         # Overloaded to accept PodcastDevice objects as arguments
  216.         device = getattr(device, 'device_id', device)
  217.         return simple.SimpleClient.put_subscriptions(self, device, urls)
  218.  
  219.     @simple.needs_credentials
  220.     def update_subscriptions(self, device_id, add_urls=[], remove_urls=[]):
  221.         """Update the subscription list for a given device.
  222.  
  223.         Returns a UpdateResult object that contains a list of (sanitized)
  224.         URLs and a "since" value that can be used for future calls to
  225.         pull_subscriptions.
  226.  
  227.         For every (old_url, new_url) tuple in the updated_urls list of
  228.         the resulting object, the client should rewrite the URL in its
  229.         subscription list so that new_url is used instead of old_url.
  230.         """
  231.         uri = self._locator.add_remove_subscriptions_uri(device_id)
  232.  
  233.         if not all(isinstance(x, basestring) for x in add_urls):
  234.             raise ValueError('add_urls must be a list of strings but was %s' % add_urls)
  235.  
  236.         if not all(isinstance(x, basestring) for x in remove_urls):
  237.             raise ValueError('remove_urls must be a list of strings but was %s' % remove_urls)
  238.  
  239.         data = {'add': add_urls, 'remove': remove_urls}
  240.         response = self._client.POST(uri, data)
  241.  
  242.         if response is None:
  243.             raise InvalidResponse('Got empty response')
  244.  
  245.         if 'timestamp' not in response:
  246.             raise InvalidResponse('Response does not contain timestamp')
  247.  
  248.         try:
  249.             since = int(response['timestamp'])
  250.         except ValueError:
  251.             raise InvalidResponse('Invalid value %s for timestamp in response' % response['timestamp'])
  252.  
  253.         if 'update_urls' not in response:
  254.             raise InvalidResponse('Response does not contain update_urls')
  255.  
  256.         try:
  257.             update_urls = [(a, b) for a, b in response['update_urls']]
  258.         except:
  259.             raise InvalidResponse('Invalid format of update_urls in response: %s' % response['update_urls'])
  260.  
  261.         if not all(isinstance(a, basestring) and isinstance(b, basestring) \
  262.                     for a, b in update_urls):
  263.             raise InvalidResponse('Invalid format of update_urls in response: %s' % update_urls)
  264.  
  265.         return UpdateResult(update_urls, since)
  266.  
  267.     @simple.needs_credentials
  268.     def pull_subscriptions(self, device_id, since=None):
  269.         """Downloads subscriptions since the time of the last update
  270.  
  271.         The "since" parameter should be a timestamp that has been
  272.         retrieved previously by a call to update_subscriptions or
  273.         pull_subscriptions.
  274.  
  275.         Returns a SubscriptionChanges object with two lists (one for
  276.         added and one for removed podcast URLs) and a "since" value
  277.         that can be used for future calls to this method.
  278.         """
  279.         uri = self._locator.subscription_updates_uri(device_id, since)
  280.         data = self._client.GET(uri)
  281.  
  282.         if data is None:
  283.             raise InvalidResponse('Got empty response')
  284.  
  285.         if 'add' not in data:
  286.             raise InvalidResponse('List of added podcasts not in response')
  287.  
  288.         if 'remove' not in data:
  289.             raise InvalidResponse('List of removed podcasts not in response')
  290.  
  291.         if 'timestamp' not in data:
  292.             raise InvalidResponse('Timestamp missing from response')
  293.  
  294.         if not all(isinstance(x, basestring) for x in data['add']):
  295.             raise InvalidResponse('Invalid value(s) in list of added podcasts: %s' % data['add'])
  296.  
  297.         if not all(isinstance(x, basestring) for x in data['remove']):
  298.             raise InvalidResponse('Invalid value(s) in list of removed podcasts: %s' % data['remove'])
  299.  
  300.         try:
  301.             since = int(data['timestamp'])
  302.         except ValueError:
  303.             raise InvalidResponse('Timestamp has invalid format in response: %s' % data['timestamp'])
  304.  
  305.         return SubscriptionChanges(data['add'], data['remove'], since)
  306.  
  307.     @simple.needs_credentials
  308.     def upload_episode_actions(self, actions=[]):
  309.         """Uploads a list of EpisodeAction objects to the server
  310.  
  311.         Returns the timestamp that can be used for retrieving changes.
  312.         """
  313.         uri = self._locator.upload_episode_actions_uri()
  314.         actions = [action.to_dictionary() for action in actions]
  315.         response = self._client.POST(uri, actions)
  316.  
  317.         if response is None:
  318.             raise InvalidResponse('Got empty response')
  319.  
  320.         if 'timestamp' not in response:
  321.             raise InvalidResponse('Response does not contain timestamp')
  322.  
  323.         try:
  324.             since = int(response['timestamp'])
  325.         except ValueError:
  326.             raise InvalidResponse('Invalid value %s for timestamp in response' % response['timestamp'])
  327.  
  328.         return since
  329.  
  330.     @simple.needs_credentials
  331.     def download_episode_actions(self, since=None,
  332.             podcast=None, device_id=None):
  333.         """Downloads a list of EpisodeAction objects from the server
  334.  
  335.         Returns a EpisodeActionChanges object with the list of
  336.         new actions and a "since" timestamp that can be used for
  337.         future calls to this method when retrieving episodes.
  338.         """
  339.         uri = self._locator.download_episode_actions_uri(since,
  340.                 podcast, device_id)
  341.         data = self._client.GET(uri)
  342.  
  343.         if data is None:
  344.             raise InvalidResponse('Got empty response')
  345.  
  346.         if 'actions' not in data:
  347.             raise InvalidResponse('Response does not contain actions')
  348.  
  349.         if 'timestamp' not in data:
  350.             raise InvalidResponse('Response does not contain timestamp')
  351.  
  352.         try:
  353.             since = int(data['timestamp'])
  354.         except ValueError:
  355.             raise InvalidResponse('Invalid value for timestamp: ' + 
  356.                     data['timestamp'])
  357.  
  358.         dicts = data['actions']
  359.         try:
  360.             actions = [EpisodeAction.from_dictionary(d) for d in dicts]
  361.         except KeyError:
  362.             raise InvalidResponse('Missing keys in action list response')
  363.  
  364.         return EpisodeActionChanges(actions, since)
  365.  
  366.     @simple.needs_credentials
  367.     def update_device_settings(self, device_id, caption=None, type=None):
  368.         """Update the description of a device on the server
  369.  
  370.         This changes the caption and/or type of a given device
  371.         on the server. If the device does not exist, it is
  372.         created with the given settings.
  373.  
  374.         The parameters caption and type are both optional and
  375.         when set to a value other than None will be used to
  376.         update the device settings.
  377.  
  378.         Returns True if the request succeeded, False otherwise.
  379.         """
  380.         uri = self._locator.device_settings_uri(device_id)
  381.         data = {}
  382.         if caption is not None:
  383.             data['caption'] = caption
  384.         if type is not None:
  385.             data['type'] = type
  386.         return (self._client.POST(uri, data) is None)
  387.  
  388.     @simple.needs_credentials
  389.     def get_devices(self):
  390.         """Returns a list of this user's PodcastDevice objects
  391.  
  392.         The resulting list can be used to display a selection
  393.         list to the user or to determine device IDs to pull
  394.         the subscription list from.
  395.         """
  396.         uri = self._locator.device_list_uri()
  397.         dicts = self._client.GET(uri)
  398.         if dicts is None:
  399.             raise InvalidResponse('No response received')
  400.  
  401.         try:
  402.             return [PodcastDevice.from_dictionary(d) for d in dicts]
  403.         except KeyError:
  404.             raise InvalidResponse('Missing keys in device list response')
  405.  
  406.     def get_favorite_episodes(self):
  407.         """Returns a List of Episode Objects containing the Users
  408.         favorite Episodes"""
  409.         uri = self._locator.favorite_episodes_uri()
  410.         return [public.Episode.from_dict(d) for d in self._client.GET(uri)]
  411.     
  412.     def get_settings(self, type, scope_param1=None, scope_param2=None):
  413.         """Returns a Dictionary with the set settings for the type & specified scope"""
  414.         uri = self._locator.settings_uri(type, scope_param1, scope_param2)
  415.         return self._client.GET(uri)
  416.     
  417.     def set_settings(self, type, scope_param1, scope_param2, set={}, remove=[]):
  418.         """Returns a Dictionary with the set settings for the type & specified scope"""
  419.         uri = self._locator.settings_uri(type, scope_param1, scope_param2)
  420.         data = {}
  421.         data["set"] = set
  422.         data["remove"] = remove
  423.         return self._client.POST(uri, data)
  424.