Here's a cute way to do this in python scripting, only from a single collection:

import bpy,random
bookYlen=0.0
bookCnt = len(bpy.data.collections['book_collection'].objects)
maxAttempts = bookCnt * 2 ## allow for dups in randon selection, up to a point
shelf = bpy.data.objects['shelf']
bpy.ops.collection.create(name='placed books')
bpy.context.scene.collection.children.link( bpy.data.collections['placed books'] )
bookNextYloc = shelf.location.y - (shelf.dimensions.y / 2)
# keep choosing and plancing an object in collection until total width will exceed
while bookYlen < (shelf.dimensions.y - .16):
attempts = 0 #
print( 'bookYlen',bookYlen)
while attempts <= maxAttempts:
attempts += 1
# get a random book
chosenIdx = random.randint( 0, bookCnt - 1 )
chosen = bpy.data.collections['book_collection'].objects[ chosenIdx ]
widthToAdd = chosen.dimensions.y
print( 'attempts',attempts,'maxAttempts',maxAttempts, 'try adding ', widthToAdd, 'at',bookYlen, 'with shelf.dimensions.y', shelf.dimensions.y)
if ((bookYlen + widthToAdd) > shelf.dimensions.y):
print( 'filling up ... try another' )
continue # hope for another, slimmer book
# place (a linkded duplicate of) this book on the shelf
chosen.select_set( True )
bpy.ops.object.duplicate_move_linked()
chosen.select_set( False )
# created object is the new book to place
book1 = bpy.context.selected_objects[0]
book1.select_set( True )
# assume x and z axis are aligned
book1.location.z = shelf.location.z
book1.location.x = shelf.location.x
# place at next open position
book1.location.y = bookNextYloc
print( book1.name, 'placed at ', bookYlen )
bookYlen += book1.dimensions.y
bookNextYloc += book1.dimensions.y
# move to "placed" collection
bpy.data.collections['placed books'].objects.link( book1 )
bpy.data.collections['book_collection'].objects.unlink( book1 )
book1.select_set( False )
