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