Home | History | Annotate | Download | only in server
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2010 Google Inc.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #     http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 """Tunes DB service implementation.
     19 
     20 This module contains all the protocol buffer and service definitions
     21 necessary for the Tunes DB service.
     22 """
     23 
     24 import base64
     25 import sys
     26 
     27 from google.appengine.ext import db
     28 
     29 from protorpc import descriptor
     30 from protorpc import message_types
     31 from protorpc import messages
     32 from protorpc import protobuf
     33 from protorpc import remote
     34 
     35 import model
     36 
     37 
     38 class Artist(messages.Message):
     39   """Musician or music group responsible for music production.
     40 
     41   Fields:
     42     artist_id: Unique opaque identifier for artist.
     43     name: User friendly name of artist.
     44     album_count: Number of albums produced by artist.
     45   """
     46 
     47   artist_id = messages.StringField(1, required=True)
     48   name = messages.StringField(2, required=True)
     49 
     50   album_count = messages.IntegerField(3)
     51 
     52 
     53 class Album(messages.Message):
     54   """Album produced by a musician or music group.
     55 
     56   Fields:
     57     album_id: Unique opaque identifier for artist.
     58     artist_id: Artist id of musician or music group that produced album.
     59     name: Name of album.
     60     released: Year when album was released.
     61   """
     62 
     63   album_id = messages.StringField(1, required=True)
     64   artist_id = messages.StringField(2, required=True)
     65   name = messages.StringField(3, required=True)
     66   released = messages.IntegerField(4)
     67 
     68 
     69 class AddArtistRequest(messages.Message):
     70   """Request to add a new Artist to library.
     71 
     72   Fields:
     73     name: User friendly name of artist.
     74   """
     75 
     76   name = messages.StringField(1, required=True)
     77 
     78 
     79 class AddArtistResponse(messages.Message):
     80   """Response sent after creation of new artist in library.
     81 
     82   Fields:
     83     artist_id: Unique opaque ID of new artist.
     84   """
     85 
     86   artist_id = messages.StringField(1, required=True)
     87 
     88 
     89 class UpdateArtistRequest(messages.Message):
     90   """Update an existing artist.
     91 
     92   Fields:
     93     artist: Complete information about artist to update.
     94   """
     95 
     96   artist = messages.MessageField(Artist, 1, required=True)
     97 
     98 
     99 class UpdateArtistResponse(messages.Message):
    100   """Artist update response.
    101 
    102   Fields:
    103     artist_updated: Artist was found and updated.
    104   """
    105 
    106   artist_updated = messages.BooleanField(1, required=True)
    107 
    108 
    109 class DeleteArtistRequest(messages.Message):
    110   """Delete artist from library.
    111 
    112   Fields:
    113     artist_id: Unique opaque ID of artist to delete.
    114   """
    115 
    116   artist_id = messages.StringField(1, required=True)
    117 
    118 
    119 class DeleteArtistResponse(messages.Message):
    120   """Artist deletion response.
    121 
    122   Fields:
    123     artist_deleted: Artist was found and deleted.
    124   """
    125 
    126   artist_deleted = messages.BooleanField(1, default=True)
    127 
    128 
    129 class FetchArtistRequest(messages.Message):
    130   """Fetch an artist from the library.
    131 
    132   Fields:
    133     artist_id: Unique opaque ID of artist to fetch.
    134   """
    135 
    136   artist_id = messages.StringField(1, required=True)
    137 
    138 
    139 class FetchArtistResponse(messages.Message):
    140   """Fetched artist from library.
    141 
    142   Fields:
    143     artist: Artist found in library.
    144   """
    145 
    146   artist = messages.MessageField(Artist, 1)
    147 
    148 
    149 class SearchArtistsRequest(messages.Message):
    150   """Artist search request.
    151 
    152   Fields:
    153     continuation: Continuation from the response of a previous call to
    154       search_artists remote method.
    155     fetch_size: Maximum number of records to retrieve.
    156     name_prefix: Name prefix of artists to search.  If none provided and
    157       no continuation provided, search will be of all artists in library.
    158       If continuation is provided, name_prefix should be empty, if not, value
    159       is ignored.
    160   """
    161 
    162   continuation = messages.StringField(1)
    163   fetch_size = messages.IntegerField(2, default=10)
    164   name_prefix = messages.StringField(3, default=u'')
    165 
    166 
    167 class SearchArtistsResponse(messages.Message):
    168   """Response from searching artists.
    169 
    170   Fields:
    171     artists: Artists found from search up to fetch_size.
    172     continuation: Opaque string that can be used with a new search request
    173       that will continue finding new artists where this response left off.
    174       Will not be set if there were no results from the search or fewer
    175       artists were returned in the response than requested, indicating the end
    176       of the query.
    177   """
    178 
    179   artists = messages.MessageField(Artist, 1, repeated=True)
    180   continuation = messages.StringField(2)
    181 
    182 
    183 class AddAlbumRequest(messages.Message):
    184   """Request to add a new album to library.
    185 
    186   Fields:
    187     name: User friendly name of album.
    188     artist_id: Artist id of artist that produced record.
    189     released: Year album was released.
    190   """
    191 
    192   name = messages.StringField(1, required=True)
    193   artist_id = messages.StringField(2, required=True)
    194   released = messages.IntegerField(3)
    195 
    196 
    197 class AddAlbumResponse(messages.Message):
    198   """Response sent after creation of new album in library.
    199 
    200   Fields:
    201     album_id: Unique opaque ID of new album.
    202   """
    203 
    204   album_id = messages.StringField(1, required=True)
    205 
    206 
    207 class UpdateAlbumRequest(messages.Message):
    208   """Update an existing album.
    209 
    210   Fields:
    211     album: Complete information about album to update.
    212   """
    213 
    214   album = messages.MessageField(Album, 1, required=True)
    215 
    216 
    217 class UpdateAlbumResponse(messages.Message):
    218   """Album update response.
    219 
    220   Fields:
    221     album_updated: Album was found and updated.
    222   """
    223 
    224   album_updated = messages.BooleanField(1, required=True)
    225 
    226 
    227 class DeleteAlbumRequest(messages.Message):
    228   """Delete album from library.
    229 
    230   Fields:
    231     album_id: Unique opaque ID of album to delete.
    232   """
    233 
    234   album_id = messages.StringField(1, required=True)
    235 
    236 
    237 class DeleteAlbumResponse(messages.Message):
    238   """Album deletion response.
    239 
    240   Fields:
    241     album_deleted: Album was found and deleted.
    242   """
    243 
    244   album_deleted = messages.BooleanField(1, default=True)
    245 
    246 
    247 class FetchAlbumRequest(messages.Message):
    248   """Fetch an album from the library.
    249 
    250   Fields:
    251     album_id: Unique opaque ID of album to fetch.
    252   """
    253 
    254   album_id = messages.StringField(1, required=True)
    255 
    256 
    257 class FetchAlbumResponse(messages.Message):
    258   """Fetched album from library.
    259 
    260   Fields:
    261     album: Album found in library.
    262   """
    263 
    264   album = messages.MessageField(Album, 1)
    265 
    266 
    267 class SearchAlbumsRequest(messages.Message):
    268   """Album search request.
    269 
    270   Fields:
    271     continuation: Continuation from the response of a previous call to
    272       search_albums remote method.
    273     fetch_size: Maximum number of records to retrieve.
    274     name_prefix: Name prefix of albms to search.  If none provided and
    275       no continuation provided, search will be of all albums in library.
    276       If continuation is provided, name_prefix should be empty, if not, value
    277       is ignored.
    278     artist_id: Restrict search to albums of single artist.
    279   """
    280 
    281   continuation = messages.StringField(1)
    282   fetch_size = messages.IntegerField(2, default=10)
    283   name_prefix = messages.StringField(3, default=u'')
    284   artist_id = messages.StringField(4)
    285 
    286 
    287 class SearchAlbumsResponse(messages.Message):
    288   """Response from searching artists.
    289 
    290   Fields:
    291     albums: Albums found from search up to fetch_size.
    292     continuation: Opaque string that can be used with a new search request
    293       that will continue finding new albums where this response left off.
    294       Will not be set if there were no results from the search or fewer
    295       albums were returned in the response than requested, indicating the end
    296       of the query.
    297   """
    298 
    299   albums = messages.MessageField(Album, 1, repeated=True)
    300   continuation = messages.StringField(2)
    301 
    302 
    303 class MusicLibraryService(remote.Service):
    304   """Music library service."""
    305 
    306   __file_set = None
    307 
    308   def __artist_from_model(self, artist_model):
    309     """Helper that copies an Artist model to an Artist message.
    310 
    311     Args:
    312       artist_model: model.ArtistInfo instance to convert in to an Artist
    313         message.
    314 
    315     Returns:
    316       New Artist message with contents of artist_model copied in to it.
    317     """
    318     return Artist(artist_id=unicode(artist_model.key()),
    319                   name=artist_model.name,
    320                   album_count=artist_model.album_count)
    321 
    322   def __album_from_model(self, album_model):
    323     """Helper that copies an Album model to an Album message.
    324 
    325     Args:
    326       album_model: model.AlbumInfo instance to convert in to an Album
    327         message.
    328 
    329     Returns:
    330       New Album message with contents of album_model copied in to it.
    331     """
    332     artist_id = model.AlbumInfo.artist.get_value_for_datastore(album_model)
    333 
    334     return Album(album_id=unicode(album_model.key()),
    335                  artist_id=unicode(artist_id),
    336                  name=album_model.name,
    337                  released=album_model.released or None)
    338 
    339   @classmethod
    340   def __search_info(cls,
    341                     request,
    342                     info_class,
    343                     model_to_message,
    344                     customize_query=None):
    345     """Search over an Info subclass.
    346 
    347     Since all search request classes are very similar, it's possible to
    348     generalize how to do searches over them.
    349 
    350     Args:
    351       request: Search request received from client.
    352       info_class: The model.Info subclass to search.
    353       model_to_method: Function (model) -> message that transforms an instance
    354         of info_class in to the appropriate messages.Message subclass.
    355       customize_query: Function (request, query) -> None that adds additional
    356         filters to Datastore query based on specifics of that search message.
    357 
    358     Returns:
    359       Tuple (results, continuation):
    360         results: A list of messages satisfying the parameters of the request.
    361           None if there are no results.
    362         continuation: Continuation string for response if there are more
    363           results available.  None if there are no more results available.
    364     """
    365     # TODO(rafek): fetch_size from this request should take priority
    366     # over what is stored in continuation.
    367     if request.continuation:
    368       encoded_search, continuation = request.continuation.split(':', 1)
    369       decoded_search = base64.urlsafe_b64decode(encoded_search.encode('utf-8'))
    370       request = protobuf.decode_message(type(request), decoded_search)
    371     else:
    372       continuation = None
    373       encoded_search = unicode(base64.urlsafe_b64encode(
    374           protobuf.encode_message(request)))
    375 
    376     name_prefix = request.name_prefix
    377 
    378     query = info_class.search(name_prefix)
    379     query.order('name')
    380     if customize_query:
    381       customize_query(request, query)
    382 
    383     if continuation:
    384       # TODO(rafek): Pure query cursors are not safe for model with
    385       # query restrictions.  Would technically need to be encrypted.
    386       query.with_cursor(continuation)
    387 
    388     fetch_size = request.fetch_size
    389 
    390     model_instance = query.fetch(fetch_size)
    391     results = None
    392     continuation = None
    393     if model_instance:
    394       results = [model_to_message(i) for i in model_instance]
    395       if len(model_instance) == fetch_size:
    396         cursor = query.cursor()
    397         continuation = u'%s:%s' % (encoded_search, query.cursor())
    398 
    399     return results, continuation
    400 
    401 
    402   @remote.method(AddArtistRequest, AddArtistResponse)
    403   def add_artist(self, request):
    404     """Add artist to library."""
    405     artist_name = request.name
    406     def do_add():
    407       artist = model.ArtistInfo(name=artist_name)
    408       artist.put()
    409       return artist
    410     artist = db.run_in_transaction(do_add)
    411 
    412     return AddArtistResponse(artist_id = unicode(artist.key()))
    413 
    414   @remote.method(UpdateArtistRequest, UpdateArtistResponse)
    415   def update_artist(self, request):
    416     """Update artist from library."""
    417     def do_deletion():
    418       artist = model.ArtistInfo.get(request.artist.artist_id)
    419       if artist:
    420         artist.name = request.artist.name
    421         artist.put()
    422         return True
    423       else:
    424         return False
    425     return UpdateArtistResponse(
    426       artist_updated=db.run_in_transaction(do_deletion))
    427 
    428   @remote.method(DeleteArtistRequest, DeleteArtistResponse)
    429   def delete_artist(self, request):
    430     """Delete artist from library."""
    431     def do_deletion():
    432       artist = model.ArtistInfo.get(request.artist_id)
    433       if artist:
    434         db.delete(model.Info.all(keys_only=True).ancestor(artist))
    435         return True
    436       else:
    437         return False
    438     return DeleteArtistResponse(
    439       artist_deleted = db.run_in_transaction(do_deletion))
    440 
    441   @remote.method(FetchArtistRequest, FetchArtistResponse)
    442   def fetch_artist(self, request):
    443     """Fetch artist from library."""
    444     artist_model = model.ArtistInfo.get(request.artist_id)
    445     if isinstance(artist_model, model.ArtistInfo):
    446       artist = self.__artist_from_model(artist_model)
    447     else:
    448       artist = None
    449     return FetchArtistResponse(artist=artist)
    450 
    451 
    452   @remote.method(SearchArtistsRequest, SearchArtistsResponse)
    453   def search_artists(self, request):
    454     """Search library for artists."""
    455     results, continuation = self.__search_info(request,
    456                                                model.ArtistInfo,
    457                                                self.__artist_from_model)
    458     return SearchArtistsResponse(artists=results or [],
    459                                  continuation=continuation or None)
    460 
    461   @remote.method(AddAlbumRequest, AddAlbumResponse)
    462   def add_album(self, request):
    463     """Add album to library."""
    464     def create_album():
    465       if not request.artist_id:
    466         raise ValueError('Request does not have artist-id.')
    467       artist = model.ArtistInfo.get(request.artist_id)
    468       if not artist:
    469         raise ValueError('No artist found for %s.' % request.artist_id)
    470       artist.album_count += 1
    471       artist.put()
    472 
    473       album = model.AlbumInfo(name=request.name,
    474                               released=request.released,
    475                               artist=artist,
    476                               parent=artist)
    477       album.put()
    478 
    479       return album
    480     album = db.run_in_transaction(create_album)
    481 
    482     return AddAlbumResponse(album_id=unicode(album.key()))
    483 
    484   @remote.method(UpdateAlbumRequest, UpdateAlbumResponse)
    485   def update_album(self, request):
    486     """Update album from library."""
    487     def do_deletion():
    488       album = model.AlbumInfo.get(request.album.album_id)
    489       if album:
    490         album.name = request.album.name
    491         album.released = request.album.released
    492         album.put()
    493         return True
    494       else:
    495         return False
    496     return UpdateAlbumResponse(album_updated=db.run_in_transaction(do_deletion))
    497 
    498   @remote.method(DeleteAlbumRequest, DeleteAlbumResponse)
    499   def delete_album(self, request):
    500     """Delete album from library."""
    501     def do_deletion():
    502       album = model.AlbumInfo.get(request.album_id)
    503 
    504       artist = album.artist
    505       artist.album_count -= 1
    506       artist.put()
    507 
    508       if album:
    509         db.delete(model.Info.all(keys_only=True).ancestor(album))
    510         return True
    511       else:
    512         return False
    513 
    514     return DeleteAlbumResponse(album_deleted=db.run_in_transaction(do_deletion))
    515 
    516   @remote.method(FetchAlbumRequest, FetchAlbumResponse)
    517   def fetch_album(self, request):
    518     """Fetch album from library."""
    519     album_model = model.AlbumInfo.get(request.album_id)
    520     if isinstance(album_model, model.AlbumInfo):
    521       album = self.__album_from_model(album_model)
    522     else:
    523       album = None
    524     return FetchAlbumResponse(album=album)
    525 
    526   @remote.method(SearchAlbumsRequest, SearchAlbumsResponse)
    527   def search_albums(self, request):
    528     """Search library for albums."""
    529     def customize_query(request, query):
    530       if request.artist_id:
    531         query.filter('artist', db.Key(request.artist_id))
    532 
    533     response = SearchAlbumsResponse()
    534     results, continuation = self.__search_info(request,
    535                                                model.AlbumInfo,
    536                                                self.__album_from_model,
    537                                                customize_query)
    538     return SearchAlbumsResponse(albums=results or [],
    539                                 continuation=continuation or None)
    540