• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
20This module contains all the protocol buffer and service definitions
21necessary for the Tunes DB service.
22"""
23
24import base64
25import sys
26
27from google.appengine.ext import db
28
29from protorpc import descriptor
30from protorpc import message_types
31from protorpc import messages
32from protorpc import protobuf
33from protorpc import remote
34
35import model
36
37
38class 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
53class 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
69class 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
79class 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
89class 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
99class 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
109class 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
119class 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
129class 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
139class 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
149class 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
167class 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
183class 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
197class 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
207class 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
217class 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
227class 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
237class 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
247class 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
257class 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
267class 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
287class 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
303class 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